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