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