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