1 # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
4 # Note: some trivial code here stolen from git-svn + Perl modules
5 # distributed with git. I wrote these long ago and retain my copyright
6 # to it, so I'm within my right to relicense as AGPLv3+. The original
7 # git versions remain GPLv2.
9 # Not using Git.pm and friends directly because some git installations may use
10 # a different Perl than this (and I might end up rewriting this entirely
11 # in C at a later time...)
15 use File::Path qw/mkpath/;
20 use Digest::SHA qw/sha1_hex/;
22 # Future versions of Ssoma will always be able to handle this version, at least
23 our $REPO_VERSION = 1;
26 my ($class, $git_dir) = @_;
29 index => "$git_dir/ssoma.index",
33 # initialize a git repository
35 my ($self, @opts) = @_;
37 my @cmd = (qw(git init --bare), @opts);
38 push @cmd, $self->{git_dir};
40 system(@cmd) == 0 or die join(' ', @cmd)." failed: $?\n";
42 $self->tmp_git_do(sub {
43 @cmd = (qw(git config ssoma.repoversion), $REPO_VERSION);
44 system(@cmd) == 0 or die "command: ". join(' ', @cmd) . ": $?\n";
48 sub lockfile { $_[0]->{git_dir} . "/ssoma.lock" }
51 my ($self, $sub) = @_;
53 my $fs = File::FcntlLock->new;
55 $fs->l_type(SEEK_CUR);
59 my $path = $self->lockfile;
62 # we must not race here because this is concurrent:
63 sysopen($lock, $path, O_WRONLY) or
64 sysopen($lock, $path, O_CREAT|O_EXCL|O_WRONLY) or
65 sysopen($lock, $path, O_WRONLY) or
66 die "failed to open lock $path: $!\n";
68 # wait for other processes to be done
69 $fs->lock($lock, F_SETLKW) or die "lock failed: " . $fs->error . "\n";
72 my @ret = eval { &$sub };
75 # these would happen anyways, but be explicit so we can detect errors
76 $fs->lock($lock, F_UNLCK) or die "unlock failed: " . $fs->error . "\n";
77 close $lock or die "close lockfile($path) failed: $!\n";
81 wantarray ? @ret : $ret[0];
84 # perform sub with the given GIT_DIR
86 my ($self, $sub) = @_;
87 local $ENV{GIT_DIR} = $self->{git_dir};
91 # perform sub with a temporary index
93 my ($self, $sub) = @_;
94 local $ENV{GIT_INDEX_FILE} = $self->{index};
96 my ($dir, $base) = ($self->{index} =~ m#^(.*?)/?([^/]+)$#);
97 mkpath([$dir]) unless -d $dir;
98 -d $dir or die "$dir creation failed $!\n";
102 # bidirectional pipe, output would be SHA-1 hexdigest
104 my ($self, @cmd) = @_;
106 my $cmd = join(' ', @cmd);
107 my ($in_0, $in_1, $out_0, $out_1);
109 pipe($in_0, $in_1) or die "pipe failed: $!\n";
110 pipe($out_0, $out_1) or die "pipe failed: $!\n";
113 defined $pid or die "fork failed: $!\n";
116 open STDIN, '<&', $in_0 or die "redirect stdin failed: $!\n";
117 open STDOUT, '>&', $out_1 or die "redirect stdout failed: $!\n";
119 die "exec($cmd) failed: $!\n";
122 close $in_0 or die "close in_0 failed: $!\n";
123 close $out_1 or die "close out_1 failed: $!\n";
125 close $in_1 or die "close in_1 failed: $!\n";
127 close $out_0 or die "close out_0 failed: $!\n";
128 waitpid($pid, 0) or die "waitpid $pid failed: $!\n";
129 $? == 0 or die "$cmd failed: $?\n";
131 $sha1 =~ /\A[a-f0-9]{40}\z/i or die "not a SHA-1: $sha1\n";
135 # run a command described by str and return the SHA-1 hexdigest output
137 my ($self, $str) = @_;
140 die "$str failed: $?\n" if $?;
142 $sha1 =~ /\A[a-f0-9]{40}\z/i or
143 die "not a SHA-1 hexdigest from: $str\n";
147 # returns a blob identifier the new message
149 my ($self, $simple) = @_;
150 $self->bidi_sha1(qw/git hash-object -w --stdin/, sub {
152 print $io $simple->as_string or die "print failed: $!\n";
156 # converts the given object name to an Email::Simple object
158 my ($self, $obj) = @_;
159 Email::Simple->new($self->cat_blob($obj));
162 # returns key-value pairs of config directives in a hash
164 my ($self, $file) = @_;
166 local $ENV{GIT_CONFIG} = $file;
168 my @cfg = `git config -l`;
169 $? == 0 or die "git config -l failed: $?\n";
171 my %rv = map { split(/=/, $_, 2) } @cfg;
175 # used to hash the relevant portions of a message when there are conflicts
177 my ($self, $simple) = @_;
178 my $dig = Digest::SHA->new("SHA-1");
179 $dig->add($simple->header("Subject"));
180 $dig->add($simple->body);
184 # we currently only compare messages for equality based on
185 # Message-ID, Subject: header and body, nothing else.
186 # both args are Email::Simple objects
188 my ($self, $cur, $new) = @_;
190 (($cur->header("Subject") eq $new->header("Subject")) &&
191 ($cur->body eq $new->body));
194 # kills leading/trailing space in-place
201 my ($self, $message_id) = @_;
202 stripws($message_id);
203 my $hex = sha1_hex($message_id);
204 $hex =~ /\A([a-f0-9]{2})([a-f0-9]{38})\z/i or
205 die "BUG: not a SHA-1 hex: $hex";
210 my ($self, $blob_id) = @_;
211 my $cmd = "git cat-file blob $blob_id";
213 die "$cmd failed: $?\n" if $?;
218 my ($self, $obj) = @_;
219 my $cmd = "git cat-file -t $obj";
221 die "$cmd failed: $?\n" if $?;
226 # only used for conflict resolution
228 my ($self, $obj, $sub) = @_;
229 my $cmd = "git ls-tree $obj";
231 $? == 0 or die "$cmd failed: $!\n";
232 my $x40 = '[a-f0-9]{40}';
233 foreach my $line (@tree) {
234 if ($line =~ m!\A100644 blob ($x40)\t($x40)$!o) {
235 my ($blob_id, $path) = ($1, $2);
236 $sub->($blob_id, $path);
238 warn "unexpected: bad line from $cmd:\n$line";
244 my ($self, $gii, $need_parent, $ref, $message) = @_;
246 # this is basically what git commit(1) does,
247 # but we use git plumbing, not porcelain
249 my $tree = $self->qx_sha1("git write-tree");
251 # can't rely on qx_sha1 since we initial commit may not have a parent
252 my $cmd = "git rev-parse $ref^0";
255 $parent = $self->qx_sha1($cmd);
257 $parent = eval { $self->qx_sha1("$cmd 2>/dev/null") };
258 if (defined $parent && $parent !~ /\A[a-f0-9]{40}\z/) {
259 die "$cmd returned bad SHA-1: $parent\n";
264 my @cmd = (qw/git commit-tree -m/, $message);
265 push @cmd, '-p', $parent if $parent;
267 my $commit = $self->qx_sha1(join(' ', @cmd));
270 @cmd = (qw/git update-ref/, $ref, $commit);
271 push @cmd, $parent if $parent; # verification
272 system(@cmd) == 0 or die "command: ". join(' ', @cmd) . ": $?\n";
275 @cmd = qw/git gc --auto/;
276 system(@cmd) == 0 or die "command: ". join(' ', @cmd) . ": $?\n";