summaryrefslogtreecommitdiff
path: root/IkiWiki/Rcs/svn.pm
blob: 9b8d4be13da881f193621a510c9f32ac3e6d355f (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki;
  3. use warnings;
  4. use strict;
  5. use IkiWiki;
  6. use POSIX qw(setlocale LC_CTYPE);
  7. hook(type => "checkconfig", id => "svn", call => sub { #{{{
  8. if (! defined $config{svnpath}) {
  9. $config{svnpath}="trunk";
  10. }
  11. if (exists $config{svnpath}) {
  12. # code depends on the path not having extraneous slashes
  13. $config{svnpath}=~tr#/#/#s;
  14. $config{svnpath}=~s/\/$//;
  15. $config{svnpath}=~s/^\///;
  16. }
  17. }); #}}}
  18. hook(type => "getsetup", id => "svn", call => sub { #{{{
  19. return
  20. svnrepo => {
  21. type => "string",
  22. default => "",
  23. example => "/svn/wiki",
  24. description => "subversion repository location",
  25. safe => 0, # path
  26. rebuild => 0,
  27. },
  28. svnpath => {
  29. type => "string",
  30. default => "trunk",
  31. description => "path inside repository where the wiki is located",
  32. safe => 0, # paranoia
  33. rebuild => 0,
  34. },
  35. historyurl => {
  36. type => "string",
  37. default => "",
  38. example => "http://svn.example.org/trunk/[[file]]",
  39. description => "viewvc url to show file history ([[file]] substituted)",
  40. safe => 1,
  41. rebuild => 1,
  42. },
  43. diffurl => {
  44. type => "string",
  45. default => "",
  46. example => "http://svn.example.org/trunk/[[file]]?root=wiki&r1=[[r1]]&r2=[[r2]]",
  47. description => "viewvc url to show a diff ([[file]], [[r1]], and [[r2]] substituted)",
  48. safe => 1,
  49. rebuild => 1,
  50. },
  51. }); #}}}
  52. # svn needs LC_CTYPE set to a UTF-8 locale, so try to find one. Any will do.
  53. sub find_lc_ctype() {
  54. my $current = setlocale(LC_CTYPE());
  55. return $current if $current =~ m/UTF-?8$/i;
  56. # Make some obvious attempts to avoid calling `locale -a`
  57. foreach my $locale ("$current.UTF-8", "en_US.UTF-8", "en_GB.UTF-8") {
  58. return $locale if setlocale(LC_CTYPE(), $locale);
  59. }
  60. # Try to get all available locales and pick the first UTF-8 one found.
  61. if (my @locale = grep(/UTF-?8$/i, `locale -a`)) {
  62. chomp @locale;
  63. return $locale[0] if setlocale(LC_CTYPE(), $locale[0]);
  64. }
  65. # fallback to the current locale
  66. return $current;
  67. } # }}}
  68. $ENV{LC_CTYPE} = $ENV{LC_CTYPE} || find_lc_ctype();
  69. sub svn_info ($$) { #{{{
  70. my $field=shift;
  71. my $file=shift;
  72. my $info=`LANG=C svn info $file`;
  73. my ($ret)=$info=~/^$field: (.*)$/m;
  74. return $ret;
  75. } #}}}
  76. sub rcs_update () { #{{{
  77. if (-d "$config{srcdir}/.svn") {
  78. if (system("svn", "update", "--quiet", $config{srcdir}) != 0) {
  79. warn("svn update failed\n");
  80. }
  81. }
  82. } #}}}
  83. sub rcs_prepedit ($) { #{{{
  84. # Prepares to edit a file under revision control. Returns a token
  85. # that must be passed into rcs_commit when the file is ready
  86. # for committing.
  87. # The file is relative to the srcdir.
  88. my $file=shift;
  89. if (-d "$config{srcdir}/.svn") {
  90. # For subversion, return the revision of the file when
  91. # editing begins.
  92. my $rev=svn_info("Revision", "$config{srcdir}/$file");
  93. return defined $rev ? $rev : "";
  94. }
  95. } #}}}
  96. sub rcs_commit ($$$;$$) { #{{{
  97. # Tries to commit the page; returns undef on _success_ and
  98. # a version of the page with the rcs's conflict markers on failure.
  99. # The file is relative to the srcdir.
  100. my $file=shift;
  101. my $message=shift;
  102. my $rcstoken=shift;
  103. my $user=shift;
  104. my $ipaddr=shift;
  105. if (defined $user) {
  106. $message="web commit by $user".(length $message ? ": $message" : "");
  107. }
  108. elsif (defined $ipaddr) {
  109. $message="web commit from $ipaddr".(length $message ? ": $message" : "");
  110. }
  111. if (-d "$config{srcdir}/.svn") {
  112. # Check to see if the page has been changed by someone
  113. # else since rcs_prepedit was called.
  114. my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint
  115. my $rev=svn_info("Revision", "$config{srcdir}/$file");
  116. if (defined $rev && defined $oldrev && $rev != $oldrev) {
  117. # Merge their changes into the file that we've
  118. # changed.
  119. if (system("svn", "merge", "--quiet", "-r$oldrev:$rev",
  120. "$config{srcdir}/$file", "$config{srcdir}/$file") != 0) {
  121. warn("svn merge -r$oldrev:$rev failed\n");
  122. }
  123. }
  124. if (system("svn", "commit", "--quiet",
  125. "--encoding", "UTF-8", "-m",
  126. possibly_foolish_untaint($message),
  127. $config{srcdir}) != 0) {
  128. my $conflict=readfile("$config{srcdir}/$file");
  129. if (system("svn", "revert", "--quiet", "$config{srcdir}/$file") != 0) {
  130. warn("svn revert failed\n");
  131. }
  132. return $conflict;
  133. }
  134. }
  135. return undef # success
  136. } #}}}
  137. sub rcs_commit_staged ($$$) {
  138. # Commits all staged changes. Changes can be staged using rcs_add,
  139. # rcs_remove, and rcs_rename.
  140. my ($message, $user, $ipaddr)=@_;
  141. if (defined $user) {
  142. $message="web commit by $user".(length $message ? ": $message" : "");
  143. }
  144. elsif (defined $ipaddr) {
  145. $message="web commit from $ipaddr".(length $message ? ": $message" : "");
  146. }
  147. if (system("svn", "commit", "--quiet",
  148. "--encoding", "UTF-8", "-m",
  149. possibly_foolish_untaint($message),
  150. $config{srcdir}) != 0) {
  151. warn("svn commit failed\n");
  152. return 1; # failure
  153. }
  154. return undef # success
  155. }
  156. sub rcs_add ($) { #{{{
  157. # filename is relative to the root of the srcdir
  158. my $file=shift;
  159. if (-d "$config{srcdir}/.svn") {
  160. my $parent=dirname($file);
  161. while (! -d "$config{srcdir}/$parent/.svn") {
  162. $file=$parent;
  163. $parent=dirname($file);
  164. }
  165. if (system("svn", "add", "--quiet", "$config{srcdir}/$file") != 0) {
  166. warn("svn add failed\n");
  167. }
  168. }
  169. } #}}}
  170. sub rcs_remove ($) { #{{{
  171. # filename is relative to the root of the srcdir
  172. my $file=shift;
  173. if (-d "$config{srcdir}/.svn") {
  174. if (system("svn", "rm", "--force", "--quiet", "$config{srcdir}/$file") != 0) {
  175. warn("svn rm failed\n");
  176. }
  177. }
  178. } #}}}
  179. sub rcs_rename ($$) { #{{{
  180. # filenames relative to the root of the srcdir
  181. my ($src, $dest)=@_;
  182. if (-d "$config{srcdir}/.svn") {
  183. # Add parent directory for $dest
  184. my $parent=dirname($dest);
  185. if (! -d "$config{srcdir}/$parent/.svn") {
  186. while (! -d "$config{srcdir}/$parent/.svn") {
  187. $parent=dirname($dest);
  188. }
  189. if (system("svn", "add", "--quiet", "$config{srcdir}/$parent") != 0) {
  190. warn("svn add $parent failed\n");
  191. }
  192. }
  193. if (system("svn", "mv", "--force", "--quiet",
  194. "$config{srcdir}/$src", "$config{srcdir}/$dest") != 0) {
  195. warn("svn rename failed\n");
  196. }
  197. }
  198. } #}}}
  199. sub rcs_recentchanges ($) { #{{{
  200. my $num=shift;
  201. my @ret;
  202. return unless -d "$config{srcdir}/.svn";
  203. eval q{
  204. use Date::Parse;
  205. use XML::SAX;
  206. use XML::Simple;
  207. };
  208. error($@) if $@;
  209. # avoid using XML::SAX::PurePerl, it's buggy with UTF-8 data
  210. my @parsers = map { ${$_}{Name} } @{XML::SAX->parsers()};
  211. do {
  212. $XML::Simple::PREFERRED_PARSER = pop @parsers;
  213. } until $XML::Simple::PREFERRED_PARSER ne 'XML::SAX::PurePerl';
  214. # --limit is only supported on Subversion 1.2.0+
  215. my $svn_version=`svn --version -q`;
  216. my $svn_limit='';
  217. $svn_limit="--limit $num"
  218. if $svn_version =~ /\d\.(\d)\.\d/ && $1 >= 2;
  219. my $svn_url=svn_info("URL", $config{srcdir});
  220. my $xml = XMLin(scalar `svn $svn_limit --xml -v log '$svn_url'`,
  221. ForceArray => [ 'logentry', 'path' ],
  222. GroupTags => { paths => 'path' },
  223. KeyAttr => { path => 'content' },
  224. );
  225. foreach my $logentry (@{$xml->{logentry}}) {
  226. my (@pages, @message);
  227. my $rev = $logentry->{revision};
  228. my $user = $logentry->{author};
  229. my $when=str2time($logentry->{date}, 'UTC');
  230. foreach my $msgline (split(/\n/, $logentry->{msg})) {
  231. push @message, { line => $msgline };
  232. }
  233. my $committype="web";
  234. if (defined $message[0] &&
  235. $message[0]->{line}=~/$config{web_commit_regexp}/) {
  236. $user=defined $2 ? "$2" : "$3";
  237. $message[0]->{line}=$4;
  238. }
  239. else {
  240. $committype="svn";
  241. }
  242. foreach my $file (keys %{$logentry->{paths}}) {
  243. if (length $config{svnpath}) {
  244. next unless $file=~/^\/\Q$config{svnpath}\E\/([^ ]+)(?:$|\s)/;
  245. $file=$1;
  246. }
  247. my $diffurl=$config{diffurl};
  248. $diffurl=~s/\[\[file\]\]/$file/g;
  249. $diffurl=~s/\[\[r1\]\]/$rev - 1/eg;
  250. $diffurl=~s/\[\[r2\]\]/$rev/g;
  251. push @pages, {
  252. page => pagename($file),
  253. diffurl => $diffurl,
  254. } if length $file;
  255. }
  256. push @ret, {
  257. rev => $rev,
  258. user => $user,
  259. committype => $committype,
  260. when => $when,
  261. message => [@message],
  262. pages => [@pages],
  263. } if @pages;
  264. return @ret if @ret >= $num;
  265. }
  266. return @ret;
  267. } #}}}
  268. sub rcs_diff ($) { #{{{
  269. my $rev=possibly_foolish_untaint(int(shift));
  270. return `svnlook diff $config{svnrepo} -r$rev --no-diff-deleted`;
  271. } #}}}
  272. sub rcs_getctime ($) { #{{{
  273. my $file=shift;
  274. my $svn_log_infoline=qr/^r\d+\s+\|\s+[^\s]+\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/;
  275. my $child = open(SVNLOG, "-|");
  276. if (! $child) {
  277. exec("svn", "log", $file) || error("svn log $file failed to run");
  278. }
  279. my $date;
  280. while (<SVNLOG>) {
  281. if (/$svn_log_infoline/) {
  282. $date=$1;
  283. }
  284. }
  285. close SVNLOG || warn "svn log $file exited $?";
  286. if (! defined $date) {
  287. warn "failed to parse svn log for $file\n";
  288. return 0;
  289. }
  290. eval q{use Date::Parse};
  291. error($@) if $@;
  292. $date=str2time($date);
  293. debug("found ctime ".localtime($date)." for $file");
  294. return $date;
  295. } #}}}
  296. 1