summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/cvs.pm
blob: 8409bebedb4cd4bb26b4119c674a673ec38a5886 (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. chdir $config{srcdir} || error("Cannot chdir to $config{srcdir}: $!");
  98. open(my $savedout, ">&STDOUT");
  99. open(STDOUT, ">", "/dev/null");
  100. my $ret = system(@cmd);
  101. open(STDOUT, ">&", $savedout);
  102. return ($ret == 0) ? 1 : 0;
  103. }
  104. sub cvs_is_controlling {
  105. my $dir=shift;
  106. $dir=$config{srcdir} unless defined($dir);
  107. return (-d "$dir/CVS") ? 1 : 0;
  108. }
  109. sub rcs_update () {
  110. return unless cvs_is_controlling;
  111. cvs_runcvs('update', '-dP');
  112. }
  113. sub rcs_prepedit ($) {
  114. # Prepares to edit a file under revision control. Returns a token
  115. # that must be passed into rcs_commit when the file is ready
  116. # for committing.
  117. # The file is relative to the srcdir.
  118. my $file=shift;
  119. return unless cvs_is_controlling;
  120. # For cvs, return the revision of the file when
  121. # editing begins.
  122. my $rev=cvs_info("Repository revision", "$file");
  123. return defined $rev ? $rev : "";
  124. }
  125. sub rcs_commit ($$$;$$) {
  126. # Tries to commit the page; returns undef on _success_ and
  127. # a version of the page with the rcs's conflict markers on failure.
  128. # The file is relative to the srcdir.
  129. my $file=shift;
  130. my $message=shift;
  131. my $rcstoken=shift;
  132. my $user=shift;
  133. my $ipaddr=shift;
  134. return unless cvs_is_controlling;
  135. if (defined $user) {
  136. $message="web commit by $user".(length $message ? ": $message" : "");
  137. }
  138. elsif (defined $ipaddr) {
  139. $message="web commit from $ipaddr".(length $message ? ": $message" : "");
  140. }
  141. # Check to see if the page has been changed by someone
  142. # else since rcs_prepedit was called.
  143. my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint
  144. my $rev=cvs_info("Repository revision", "$config{srcdir}/$file");
  145. if (defined $rev && defined $oldrev && $rev != $oldrev) {
  146. # Merge their changes into the file that we've
  147. # changed.
  148. cvs_runcvs('update', $file) ||
  149. warn("cvs merge from $oldrev to $rev failed\n");
  150. }
  151. if (! cvs_runcvs('commit', '-m',
  152. IkiWiki::possibly_foolish_untaint $message)) {
  153. my $conflict=readfile("$config{srcdir}/$file");
  154. cvs_runcvs('update', '-C', $file) ||
  155. warn("cvs revert failed\n");
  156. return $conflict;
  157. }
  158. return undef # success
  159. }
  160. sub rcs_commit_staged ($$$) {
  161. # Commits all staged changes. Changes can be staged using rcs_add,
  162. # rcs_remove, and rcs_rename.
  163. my ($message, $user, $ipaddr)=@_;
  164. if (defined $user) {
  165. $message="web commit by $user".(length $message ? ": $message" : "");
  166. }
  167. elsif (defined $ipaddr) {
  168. $message="web commit from $ipaddr".(length $message ? ": $message" : "");
  169. }
  170. if (! cvs_runcvs('commit', '-m',
  171. IkiWiki::possibly_foolish_untaint $message)) {
  172. warn "cvs staged commit failed\n";
  173. return 1; # failure
  174. }
  175. return undef # success
  176. }
  177. sub rcs_add ($) {
  178. # filename is relative to the root of the srcdir
  179. my $file=shift;
  180. my $parent=IkiWiki::dirname($file);
  181. my @files_to_add = ($file);
  182. eval q{use File::MimeInfo};
  183. error($@) if $@;
  184. until ((length($parent) == 0) || cvs_is_controlling("$config{srcdir}/$parent")){
  185. push @files_to_add, $parent;
  186. $parent = IkiWiki::dirname($parent);
  187. }
  188. while ($file = pop @files_to_add) {
  189. if (@files_to_add == 0) {
  190. # file
  191. my $filemime = File::MimeInfo::default($file);
  192. if (defined($filemime) && $filemime eq 'text/plain') {
  193. cvs_runcvs('add', $file) ||
  194. warn("cvs add $file failed\n");
  195. } else {
  196. cvs_runcvs('add', '-kb', $file) ||
  197. warn("cvs add binary $file failed\n");
  198. }
  199. } else {
  200. # directory
  201. cvs_runcvs('add', $file) ||
  202. warn("cvs add $file failed\n");
  203. }
  204. }
  205. }
  206. sub rcs_remove ($) {
  207. # filename is relative to the root of the srcdir
  208. my $file=shift;
  209. return unless cvs_is_controlling;
  210. cvs_runcvs('rm', '-f', $file) ||
  211. warn("cvs rm $file failed\n");
  212. }
  213. sub rcs_rename ($$) {
  214. # filenames relative to the root of the srcdir
  215. my ($src, $dest)=@_;
  216. return unless cvs_is_controlling;
  217. chdir $config{srcdir} || error("Cannot chdir to $config{srcdir}: $!");
  218. if (system("mv", "$src", "$dest") != 0) {
  219. warn("filesystem rename failed\n");
  220. }
  221. rcs_add($dest);
  222. rcs_remove($src);
  223. }
  224. sub rcs_recentchanges($) {
  225. my $num = shift;
  226. my @ret;
  227. return unless cvs_is_controlling;
  228. eval q{use Date::Parse};
  229. error($@) if $@;
  230. chdir $config{srcdir} || error("Cannot chdir to $config{srcdir}: $!");
  231. # There's no cvsps option to get the last N changesets.
  232. # Write full output to a temp file and read backwards.
  233. eval q{use File::Temp qw/tempfile/};
  234. error($@) if $@;
  235. eval q{use File::ReadBackwards};
  236. error($@) if $@;
  237. my (undef, $tmpfile) = tempfile(OPEN=>0);
  238. system("env TZ=UTC cvsps -q --cvs-direct -z 30 -x >$tmpfile");
  239. if ($? == -1) {
  240. error "couldn't run cvsps: $!\n";
  241. } elsif (($? >> 8) != 0) {
  242. error "cvsps exited " . ($? >> 8) . ": $!\n";
  243. }
  244. tie(*SPSVC, 'File::ReadBackwards', $tmpfile)
  245. || error "couldn't open $tmpfile for read: $!\n";
  246. while (my $line = <SPSVC>) {
  247. $line =~ /^$/ || error "expected blank line, got $line";
  248. my ($rev, $user, $committype, $when);
  249. my (@message, @pages);
  250. # We're reading backwards.
  251. # Forwards, an entry looks like so:
  252. # ---------------------
  253. # PatchSet $rev
  254. # Date: $when
  255. # Author: $user (or user CGI runs as, for web commits)
  256. # Branch: branch
  257. # Tag: tag
  258. # Log:
  259. # @message_lines
  260. # Members:
  261. # @pages (and revisions)
  262. #
  263. while ($line = <SPSVC>) {
  264. last if ($line =~ /^Members:/);
  265. for ($line) {
  266. s/^\s+//;
  267. s/\s+$//;
  268. }
  269. my ($page, $revs) = split(/:/, $line);
  270. my ($oldrev, $newrev) = split(/->/, $revs);
  271. $oldrev =~ s/INITIAL/0/;
  272. $newrev =~ s/\(DEAD\)//;
  273. my $diffurl = defined $config{diffurl} ? $config{diffurl} : "";
  274. $diffurl=~s/\[\[file\]\]/$page/g;
  275. $diffurl=~s/\[\[r1\]\]/$oldrev/g;
  276. $diffurl=~s/\[\[r2\]\]/$newrev/g;
  277. unshift @pages, {
  278. page => pagename($page),
  279. diffurl => $diffurl,
  280. } if length $page;
  281. }
  282. while ($line = <SPSVC>) {
  283. last if ($line =~ /^Log:$/);
  284. chomp $line;
  285. unshift @message, { line => $line };
  286. }
  287. $committype = "web";
  288. if (defined $message[0] &&
  289. $message[0]->{line}=~/$config{web_commit_regexp}/) {
  290. $user=defined $2 ? "$2" : "$3";
  291. $message[0]->{line}=$4;
  292. } else {
  293. $committype="cvs";
  294. }
  295. $line = <SPSVC>; # Tag
  296. $line = <SPSVC>; # Branch
  297. $line = <SPSVC>;
  298. if ($line =~ /^Author: (.*)$/) {
  299. $user = $1 unless defined $user && length $user;
  300. } else {
  301. error "expected Author, got $line";
  302. }
  303. $line = <SPSVC>;
  304. if ($line =~ /^Date: (.*)$/) {
  305. $when = str2time($1, 'UTC');
  306. } else {
  307. error "expected Date, got $line";
  308. }
  309. $line = <SPSVC>;
  310. if ($line =~ /^PatchSet (.*)$/) {
  311. $rev = $1;
  312. } else {
  313. error "expected PatchSet, got $line";
  314. }
  315. $line = <SPSVC>; # ---------------------
  316. push @ret, {
  317. rev => $rev,
  318. user => $user,
  319. committype => $committype,
  320. when => $when,
  321. message => [@message],
  322. pages => [@pages],
  323. } if @pages;
  324. last if @ret >= $num;
  325. }
  326. unlink($tmpfile) || error "couldn't unlink $tmpfile: $!\n";
  327. return @ret;
  328. }
  329. sub rcs_diff ($) {
  330. my $rev=IkiWiki::possibly_foolish_untaint(int(shift));
  331. chdir $config{srcdir} || error("Cannot chdir to $config{srcdir}: $!");
  332. # diff output is unavoidably preceded by the cvsps PatchSet entry
  333. my @cvsps = `env TZ=UTC cvsps -q --cvs-direct -z 30 -g -s $rev`;
  334. my $blank_lines_seen = 0;
  335. while (my $line = shift @cvsps) {
  336. $blank_lines_seen++ if ($line =~ /^$/);
  337. last if $blank_lines_seen == 2;
  338. }
  339. if (wantarray) {
  340. return @cvsps;
  341. } else {
  342. return join("", @cvsps);
  343. }
  344. }
  345. sub rcs_getctime ($) {
  346. my $file=shift;
  347. my $cvs_log_infoline=qr/^date: (.+);\s+author/;
  348. open CVSLOG, "cvs -Q log -r1.1 '$file' |"
  349. || error "couldn't get cvs log output: $!\n";
  350. my $date;
  351. while (<CVSLOG>) {
  352. if (/$cvs_log_infoline/) {
  353. $date=$1;
  354. }
  355. }
  356. close CVSLOG || warn "cvs log $file exited $?";
  357. if (! defined $date) {
  358. warn "failed to parse cvs log for $file\n";
  359. return 0;
  360. }
  361. eval q{use Date::Parse};
  362. error($@) if $@;
  363. $date=str2time($date, 'UTC');
  364. debug("found ctime ".localtime($date)." for $file");
  365. return $date;
  366. }
  367. 1