git.pl 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. # Copyright (C) 2011 Alex Schroeder <alex@gnu.org>
  2. # This program is free software: you can redistribute it and/or modify it under
  3. # the terms of the GNU General Public License as published by the Free Software
  4. # Foundation, either version 3 of the License, or (at your option) any later
  5. # version.
  6. #
  7. # This program is distributed in the hope that it will be useful, but WITHOUT
  8. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  9. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  10. #
  11. # You should have received a copy of the GNU General Public License along with
  12. # this program. If not, see <http://www.gnu.org/licenses/>.
  13. use strict;
  14. use v5.10;
  15. =head1 NAME
  16. git - An Oddmuse module to save all changes made into a git repository.
  17. =head1 INSTALLATION
  18. This module is easily installable; move this file into the B<modules>
  19. subdirectory for your data directory (C<$DataDir>).
  20. =cut
  21. =head1 CONFIGURATION
  22. Set these variables in the B<config> file within your data directory.
  23. =head2 $GitBinary
  24. Default: C<git>
  25. The fully qualified name for the binary to run. Your PATH will not be searched.
  26. =head2 $GitRepo
  27. Default: C<$DataDir/git>
  28. The directory in which the repository resides. If it doesn't exist,
  29. Oddmuse will create it for you.
  30. =head2 $GitMail
  31. Default: C<unknown@oddmuse.org>
  32. The email address used to identify users in git.
  33. =head2 $GitDebug
  34. Default: 0
  35. If set, we capture the output of the git command and store it in
  36. $GitResult. This is useful when writing tests.
  37. =head2 $GitResult
  38. If $GitDebug is set, this variable holds STDOUT of the git command.
  39. =cut
  40. use Cwd;
  41. use File::Temp ();
  42. AddModuleDescription('git.pl', 'Git Extension');
  43. our ($q, %Page, %Action, %IndexHash, @IndexList, @MyInitVariables, @MyMaintenance, $DataDir);
  44. our ($GitBinary, $GitRepo, $GitMail, $GitPageFile, $GitDebug, $GitResult);
  45. $GitBinary = 'git';
  46. $GitMail = 'unknown@oddmuse.org';
  47. $GitPageFile = 0;
  48. push(@MyInitVariables, \&GitInitVariables);
  49. sub GitRun {
  50. my $oldDir = cwd;
  51. my $exitStatus;
  52. # warn join(' ', $GitBinary, @_) . "\n";
  53. ChangeDir($GitRepo);
  54. if ($GitDebug) {
  55. # TODO use ToString here
  56. # capture the output of the git comand in a temporary file
  57. my $fh = File::Temp->new();
  58. open(my $oldout, ">&STDOUT") or die "Can't dup STDOUT: $!";
  59. open(STDOUT, '>', $fh) or die "Can't redirect STDOUT: $!";
  60. # run git in the work directory
  61. $exitStatus = system($GitBinary, @_);
  62. # read the temporary file with the output
  63. close($fh);
  64. open(STDOUT, ">&", $oldout) or die "Can't dup \$oldout: $!";
  65. open(my $F, '<', $fh) or die "Can't open temp file for reading: $!";
  66. local $/ = undef; # Read complete files
  67. $GitResult = <$F>;
  68. close($F);
  69. } else {
  70. $exitStatus = system($GitBinary, @_);
  71. }
  72. ChangeDir($oldDir);
  73. return $exitStatus;
  74. }
  75. sub GitInitVariables {
  76. $GitRepo = $DataDir . '/git';
  77. }
  78. sub GitInitRepository {
  79. return if IsDir("$GitRepo/.git");
  80. my $exception = shift;
  81. CreateDir($GitRepo);
  82. GitRun(qw(init --quiet));
  83. # Add legacy pages: If you installed this extension for an older
  84. # wiki, all the existing pages need to be added. We do this for all
  85. # the pages except for the one we just saved. That page will get a
  86. # better author and log message.
  87. foreach my $id (AllPagesList()) {
  88. next if $id eq $exception;
  89. OpenPage($id);
  90. WriteStringToFile("$GitRepo/$id", $GitPageFile ? EncodePage(%Page) : $Page{text});
  91. GitRun(qw(add --), $id);
  92. }
  93. GitRun(qw(commit --quiet -m), 'initial import', "--author=Oddmuse <$GitMail>");
  94. }
  95. *GitOldSave = \&Save;
  96. *Save = \&GitNewSave;
  97. sub GitNewSave {
  98. # Save is called within lock, with opened page. That's why we cannot
  99. # call GitInitRepository right away, because it opens all the legacy
  100. # pages to save them, too. We need to save first.
  101. GitOldSave(@_);
  102. # We also need to save all the data from the open page.
  103. my $message = $Page{summary};
  104. $message =~ s/^\s+$//;
  105. $message ||= T('no summary available');
  106. my $author = $Page{username} || T('Anonymous');
  107. my $data = $GitPageFile ? EncodePage(%Page) : $Page{text};
  108. my $id = shift;
  109. # GitInitRepository will try to add and commit all the pages already
  110. # in the wiki. These are assumed to be legacy pages. The page we
  111. # just saved, however, should not be committed as a legacy page
  112. # because legacy pages are committed with a default author and log
  113. # message!
  114. GitInitRepository($id);
  115. WriteStringToFile("$GitRepo/$id", $data);
  116. GitRun(qw(add --), $id);
  117. GitRun(qw(commit --quiet -m), $message,
  118. "--author=$author <$GitMail>", '--', $id);
  119. }
  120. *GitOldDeletePage = \&DeletePage;
  121. *DeletePage = \&GitNewDeletePage;
  122. sub GitNewDeletePage {
  123. my $error = GitOldDeletePage(@_);
  124. return $error if $error;
  125. GitInitRepository();
  126. my ($id) = @_;
  127. GitRun(qw(rm --quiet --ignore-unmatch --), $id);
  128. my $message = T('page was marked for deletion');
  129. my $author = T('Oddmuse');
  130. GitRun(qw(commit --quiet -m), $message,
  131. "--author=$author <$GitMail>", '--', $id);
  132. return ''; # no error
  133. }
  134. push(@MyMaintenance, \&GitCleanup);
  135. $Action{git} = \&DoGitCleanup;
  136. sub DoGitCleanup {
  137. UserIsAdminOrError();
  138. print GetHeader('', 'Git', '');
  139. print $q->start_div({-class=>'content git'});
  140. RequestLockOrError();
  141. print $q->p(T('Main lock obtained.')), '<p>', T('Cleaning up git repository');
  142. GitCleanup();
  143. ReleaseLock();
  144. print $q->p(T('Main lock released.')), $q->end_div();
  145. PrintFooter();
  146. }
  147. sub GitCleanup {
  148. if (IsDir($GitRepo)) {
  149. print $q->p('Git cleanup starting');
  150. AllPagesList();
  151. # delete all the files including all the files starting with a dot
  152. opendir(DIR, encode_utf8($GitRepo)) or ReportError("cannot open directory $GitRepo: $!");
  153. foreach my $file (readdir(DIR)) {
  154. my $name = decode_utf8($file);
  155. next if $file eq '.git' or $file eq '.' or $file eq '..' or $IndexHash{$name};
  156. print $q->p("Deleting left over file $name");
  157. Unlink("$GitRepo/$file") or ReportError("cannot delete $GitRepo/$name: $!");
  158. }
  159. closedir DIR;
  160. # write all the files again, just to be sure
  161. print $q->p('Rewriting all the files, just to be sure');
  162. foreach my $id (@IndexList) {
  163. OpenPage($id);
  164. WriteStringToFile("$GitRepo/$id", $GitPageFile ? EncodePage(%Page) : $Page{text});
  165. }
  166. # run git!
  167. # add any new files
  168. print $q->p('Adding new files, if any');
  169. GitRun(qw(add -A));
  170. # commit the new state
  171. print $q->p('Committing changes, if any');
  172. my $exitStatus = GitRun(qw(commit --quiet -m), 'maintenance job',
  173. "--author=Oddmuse <$GitMail>");
  174. print $q->p('git commit finished with ' . $exitStatus . ' exit status.');
  175. print $q->p('Git done');
  176. }
  177. }