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