summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/cvs.pm
blob: 939e892705a5005c95aee694620a8514a62a5217 (plain)
  1. #!/usr/pkg/bin/perl
  2. package IkiWiki::Plugin::cvs;
  3. use warnings;
  4. use strict;
  5. use IkiWiki;
  6. use File::chdir;
  7. sub import {
  8. hook(type => "wrapperargcheck", id => "cvs", call => \&wrapperargcheck);
  9. hook(type => "checkconfig", id => "cvs", call => \&checkconfig);
  10. hook(type => "getsetup", id => "cvs", call => \&getsetup);
  11. hook(type => "rcs", id => "rcs_update", call => \&rcs_update);
  12. hook(type => "rcs", id => "rcs_prepedit", call => \&rcs_prepedit);
  13. hook(type => "rcs", id => "rcs_commit", call => \&rcs_commit);
  14. hook(type => "rcs", id => "rcs_commit_staged", call => \&rcs_commit_staged);
  15. hook(type => "rcs", id => "rcs_add", call => \&rcs_add);
  16. hook(type => "rcs", id => "rcs_remove", call => \&rcs_remove);
  17. hook(type => "rcs", id => "rcs_rename", call => \&rcs_rename);
  18. hook(type => "rcs", id => "rcs_recentchanges", call => \&rcs_recentchanges);
  19. hook(type => "rcs", id => "rcs_diff", call => \&rcs_diff);
  20. hook(type => "rcs", id => "rcs_getctime", call => \&rcs_getctime);
  21. }
  22. sub wrapperargcheck () {
  23. my $check_args=<<"EOF";
  24. int j;
  25. for (j = 1; j < argc; j++)
  26. if (strstr(argv[j], "New directory") != NULL)
  27. return 0;
  28. return 1;
  29. EOF
  30. return $check_args;
  31. }
  32. sub checkconfig () {
  33. if (! defined $config{cvspath}) {
  34. $config{cvspath}="ikiwiki";
  35. }
  36. if (exists $config{cvspath}) {
  37. # code depends on the path not having extraneous slashes
  38. $config{cvspath}=~tr#/#/#s;
  39. $config{cvspath}=~s/\/$//;
  40. $config{cvspath}=~s/^\///;
  41. }
  42. if (defined $config{cvs_wrapper} && length $config{cvs_wrapper}) {
  43. push @{$config{wrappers}}, {
  44. wrapper => $config{cvs_wrapper},
  45. wrappermode => (defined $config{cvs_wrappermode} ? $config{cvs_wrappermode} : "04755"),
  46. };
  47. }
  48. }
  49. sub getsetup () {
  50. return
  51. plugin => {
  52. safe => 0, # rcs plugin
  53. rebuild => undef,
  54. },
  55. cvsrepo => {
  56. type => "string",
  57. example => "/cvs/wikirepo",
  58. description => "cvs repository location",
  59. safe => 0, # path
  60. rebuild => 0,
  61. },
  62. cvspath => {
  63. type => "string",
  64. example => "ikiwiki",
  65. description => "path inside repository where the wiki is located",
  66. safe => 0, # paranoia
  67. rebuild => 0,
  68. },
  69. cvs_wrapper => {
  70. type => "string",
  71. example => "/cvs/wikirepo/CVSROOT/post-commit",
  72. description => "cvs post-commit hook to generate (triggered by CVSROOT/loginfo entry)",
  73. safe => 0, # file
  74. rebuild => 0,
  75. },
  76. cvs_wrappermode => {
  77. type => "string",
  78. example => '04755',
  79. description => "mode for cvs_wrapper (can safely be made suid)",
  80. safe => 0,
  81. rebuild => 0,
  82. },
  83. historyurl => {
  84. type => "string",
  85. example => "http://cvs.example.org/cvsweb.cgi/ikiwiki/[[file]]",
  86. description => "cvsweb url to show file history ([[file]] substituted)",
  87. safe => 1,
  88. rebuild => 1,
  89. },
  90. diffurl => {
  91. type => "string",
  92. example => "http://cvs.example.org/cvsweb.cgi/ikiwiki/[[file]].diff?r1=text&amp;tr1=[[r1]]&amp;r2=text&amp;tr2=[[r2]]",
  93. description => "cvsweb url to show a diff ([[file]], [[r1]], and [[r2]] substituted)",
  94. safe => 1,
  95. rebuild => 1,
  96. },
  97. }
  98. sub cvs_info ($$) {
  99. my $field=shift;
  100. my $file=shift;
  101. local $CWD = $config{srcdir};
  102. my $info=`cvs status $file`;
  103. my ($ret)=$info=~/^\s*$field:\s*(\S+)/m;
  104. return $ret;
  105. }
  106. sub cvs_runcvs(@) {
  107. my @cmd = @_;
  108. unshift @cmd, 'cvs', '-Q';
  109. local $CWD = $config{srcdir};
  110. open(my $savedout, ">&STDOUT");
  111. open(STDOUT, ">", "/dev/null");
  112. my $ret = system(@cmd);
  113. open(STDOUT, ">&", $savedout);
  114. return ($ret == 0) ? 1 : 0;
  115. }
  116. sub cvs_is_controlling {
  117. my $dir=shift;
  118. $dir=$config{srcdir} unless defined($dir);
  119. return (-d "$dir/CVS") ? 1 : 0;
  120. }
  121. sub rcs_update () {
  122. return unless cvs_is_controlling;
  123. cvs_runcvs('update', '-dP');
  124. }
  125. sub rcs_prepedit ($) {
  126. # Prepares to edit a file under revision control. Returns a token
  127. # that must be passed into rcs_commit when the file is ready
  128. # for committing.
  129. # The file is relative to the srcdir.
  130. my $file=shift;
  131. return unless cvs_is_controlling;
  132. # For cvs, return the revision of the file when
  133. # editing begins.
  134. my $rev=cvs_info("Repository revision", "$file");
  135. return defined $rev ? $rev : "";
  136. }
  137. sub rcs_commit ($$$;$$) {
  138. # Tries to commit the page; returns undef on _success_ and
  139. # a version of the page with the rcs's conflict markers on failure.
  140. # The file is relative to the srcdir.
  141. my $file=shift;
  142. my $message=shift;
  143. my $rcstoken=shift;
  144. my $user=shift;
  145. my $ipaddr=shift;
  146. return unless cvs_is_controlling;
  147. if (defined $user) {
  148. $message="web commit by $user".(length $message ? ": $message" : "");
  149. }
  150. elsif (defined $ipaddr) {
  151. $message="web commit from $ipaddr".(length $message ? ": $message" : "");
  152. }
  153. # Check to see if the page has been changed by someone
  154. # else since rcs_prepedit was called.
  155. my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint
  156. my $rev=cvs_info("Repository revision", "$config{srcdir}/$file");
  157. if (defined $rev && defined $oldrev && $rev != $oldrev) {
  158. # Merge their changes into the file that we've
  159. # changed.
  160. cvs_runcvs('update', $file) ||
  161. warn("cvs merge from $oldrev to $rev failed\n");
  162. }
  163. if (! cvs_runcvs('commit', '-m',
  164. IkiWiki::possibly_foolish_untaint $message)) {
  165. my $conflict=readfile("$config{srcdir}/$file");
  166. cvs_runcvs('update', '-C', $file) ||
  167. warn("cvs revert failed\n");
  168. return $conflict;
  169. }
  170. return undef # success
  171. }
  172. sub rcs_commit_staged ($$$) {
  173. # Commits all staged changes. Changes can be staged using rcs_add,
  174. # rcs_remove, and rcs_rename.
  175. my ($message, $user, $ipaddr)=@_;
  176. if (defined $user) {
  177. $message="web commit by $user".(length $message ? ": $message" : "");
  178. }
  179. elsif (defined $ipaddr) {
  180. $message="web commit from $ipaddr".(length $message ? ": $message" : "");
  181. }
  182. if (! cvs_runcvs('commit', '-m',
  183. IkiWiki::possibly_foolish_untaint $message)) {
  184. warn "cvs staged commit failed\n";
  185. return 1; # failure
  186. }
  187. return undef # success
  188. }
  189. sub rcs_add ($) {
  190. # filename is relative to the root of the srcdir
  191. my $file=shift;
  192. my $parent=IkiWiki::dirname($file);
  193. my @files_to_add = ($file);
  194. eval q{use File::MimeInfo};
  195. error($@) if $@;
  196. until ((length($parent) == 0) || cvs_is_controlling("$config{srcdir}/$parent")){
  197. push @files_to_add, $parent;
  198. $parent = IkiWiki::dirname($parent);
  199. }
  200. while ($file = pop @files_to_add) {
  201. if (@files_to_add == 0) {
  202. # file
  203. my $filemime = File::MimeInfo::default($file);
  204. if (defined($filemime) && $filemime eq 'text/plain') {
  205. cvs_runcvs('add', $file) ||
  206. warn("cvs add $file failed\n");
  207. }
  208. else {
  209. cvs_runcvs('add', '-kb', $file) ||
  210. warn("cvs add binary $file failed\n");
  211. }
  212. }
  213. else {
  214. # directory
  215. cvs_runcvs('add', $file) ||
  216. warn("cvs add $file failed\n");
  217. }
  218. }
  219. }
  220. sub rcs_remove ($) {
  221. # filename is relative to the root of the srcdir
  222. my $file=shift;
  223. return unless cvs_is_controlling;
  224. cvs_runcvs('rm', '-f', $file) ||
  225. warn("cvs rm $file failed\n");
  226. }
  227. sub rcs_rename ($$) {
  228. # filenames relative to the root of the srcdir
  229. my ($src, $dest)=@_;
  230. return unless cvs_is_controlling;
  231. local $CWD = $config{srcdir};
  232. if (system("mv", "$src", "$dest") != 0) {
  233. warn("filesystem rename failed\n");
  234. }
  235. rcs_add($dest);
  236. rcs_remove($src);
  237. }
  238. sub rcs_recentchanges($) {
  239. my $num = shift;
  240. my @ret;
  241. return unless cvs_is_controlling;
  242. eval q{use Date::Parse};
  243. error($@) if $@;
  244. local $CWD = $config{srcdir};
  245. # There's no cvsps option to get the last N changesets.
  246. # Write full output to a temp file and read backwards.
  247. eval q{use File::Temp qw/tempfile/};
  248. error($@) if $@;
  249. eval q{use File::ReadBackwards};
  250. error($@) if $@;
  251. my (undef, $tmpfile) = tempfile(OPEN=>0);
  252. system("env TZ=UTC cvsps -q --cvs-direct -z 30 -x >$tmpfile");
  253. if ($? == -1) {
  254. error "couldn't run cvsps: $!\n";
  255. } elsif (($? >> 8) != 0) {
  256. error "cvsps exited " . ($? >> 8) . ": $!\n";
  257. }
  258. tie(*SPSVC, 'File::ReadBackwards', $tmpfile)
  259. || error "couldn't open $tmpfile for read: $!\n";
  260. while (my $line = <SPSVC>) {
  261. $line =~ /^$/ || error "expected blank line, got $line";
  262. my ($rev, $user, $committype, $when);
  263. my (@message, @pages);
  264. # We're reading backwards.
  265. # Forwards, an entry looks like so:
  266. # ---------------------
  267. # PatchSet $rev
  268. # Date: $when
  269. # Author: $user (or user CGI runs as, for web commits)
  270. # Branch: branch
  271. # Tag: tag
  272. # Log:
  273. # @message_lines
  274. # Members:
  275. # @pages (and revisions)
  276. #
  277. while ($line = <SPSVC>) {
  278. last if ($line =~ /^Members:/);
  279. for ($line) {
  280. s/^\s+//;
  281. s/\s+$//;
  282. }
  283. my ($page, $revs) = split(/:/, $line);
  284. my ($oldrev, $newrev) = split(/->/, $revs);
  285. $oldrev =~ s/INITIAL/0/;
  286. $newrev =~ s/\(DEAD\)//;
  287. my $diffurl = defined $config{diffurl} ? $config{diffurl} : "";
  288. $diffurl=~s/\[\[file\]\]/$page/g;
  289. $diffurl=~s/\[\[r1\]\]/$oldrev/g;
  290. $diffurl=~s/\[\[r2\]\]/$newrev/g;
  291. unshift @pages, {
  292. page => pagename($page),
  293. diffurl => $diffurl,
  294. } if length $page;
  295. }
  296. while ($line = <SPSVC>) {
  297. last if ($line =~ /^Log:$/);
  298. chomp $line;
  299. unshift @message, { line => $line };
  300. }
  301. $committype = "web";
  302. if (defined $message[0] &&
  303. $message[0]->{line}=~/$config{web_commit_regexp}/) {
  304. $user=defined $2 ? "$2" : "$3";
  305. $message[0]->{line}=$4;
  306. }
  307. else {
  308. $committype="cvs";
  309. }
  310. $line = <SPSVC>; # Tag
  311. $line = <SPSVC>; # Branch
  312. $line = <SPSVC>;
  313. if ($line =~ /^Author: (.*)$/) {
  314. $user = $1 unless defined $user && length $user;
  315. }
  316. else {
  317. error "expected Author, got $line";
  318. }
  319. $line = <SPSVC>;
  320. if ($line =~ /^Date: (.*)$/) {
  321. $when = str2time($1, 'UTC');
  322. }
  323. else {
  324. error "expected Date, got $line";
  325. }
  326. $line = <SPSVC>;
  327. if ($line =~ /^PatchSet (.*)$/) {
  328. $rev = $1;
  329. }
  330. else {
  331. error "expected PatchSet, got $line";
  332. }
  333. $line = <SPSVC>; # ---------------------
  334. push @ret, {
  335. rev => $rev,
  336. user => $user,
  337. committype => $committype,
  338. when => $when,
  339. message => [@message],
  340. pages => [@pages],
  341. } if @pages;
  342. last if @ret >= $num;
  343. }
  344. unlink($tmpfile) || error "couldn't unlink $tmpfile: $!\n";
  345. return @ret;
  346. }
  347. sub rcs_diff ($) {
  348. my $rev=IkiWiki::possibly_foolish_untaint(int(shift));
  349. local $CWD = $config{srcdir};
  350. # diff output is unavoidably preceded by the cvsps PatchSet entry
  351. my @cvsps = `env TZ=UTC cvsps -q --cvs-direct -z 30 -g -s $rev`;
  352. my $blank_lines_seen = 0;
  353. while (my $line = shift @cvsps) {
  354. $blank_lines_seen++ if ($line =~ /^$/);
  355. last if $blank_lines_seen == 2;
  356. }
  357. if (wantarray) {
  358. return @cvsps;
  359. }
  360. else {
  361. return join("", @cvsps);
  362. }
  363. }
  364. sub rcs_getctime ($) {
  365. my $file=shift;
  366. my $cvs_log_infoline=qr/^date: (.+);\s+author/;
  367. open CVSLOG, "cvs -Q log -r1.1 '$file' |"
  368. || error "couldn't get cvs log output: $!\n";
  369. my $date;
  370. while (<CVSLOG>) {
  371. if (/$cvs_log_infoline/) {
  372. $date=$1;
  373. }
  374. }
  375. close CVSLOG || warn "cvs log $file exited $?";
  376. if (! defined $date) {
  377. warn "failed to parse cvs log for $file\n";
  378. return 0;
  379. }
  380. eval q{use Date::Parse};
  381. error($@) if $@;
  382. $date=str2time($date, 'UTC');
  383. debug("found ctime ".localtime($date)." for $file");
  384. return $date;
  385. }
  386. 1