1 package Gitalist::Model::Git;
4 use namespace::autoclean;
6 BEGIN { extends 'Catalyst::Model' }
13 use DateTime::Format::Mail;
14 use File::Stat::ModeString;
15 use List::MoreUtils qw/any/;
16 use Scalar::Util qw/blessed/;
17 use MooseX::Types::Common::String qw/NonEmptySimpleStr/; # FIXME, use Types::Path::Class and coerce
19 has project => ( isa => NonEmptySimpleStr, is => 'rw');
20 has repo_dir => ( isa => NonEmptySimpleStr, is => 'ro', lazy_build => 1 ); # Fixme - path::class
21 has git => ( isa => NonEmptySimpleStr, is => 'ro', lazy_build => 1 );
25 $self->git; # Cause lazy value build.
32 #isa => 'Git::PurePerl'
38 return Git::PurePerl->new(
39 directory => $self->project_dir( $self->project )
45 my $git = File::Which::which('git');
49 Could not find a git executable.
50 Please specify the which git executable to use in gitweb.yml
58 return Gitalist->config->{repo_dir};
62 $_[0]->gpp->get_object($_[1]);
66 my ($self, $dir) = @_;
68 return -f $dir->file('HEAD') || -f $dir->file('.git/HEAD');
72 my ($self, @args) = @_;
74 print STDERR 'RUNNING: ', $self->git, qq[ @args], $/;
76 open my $fh, '-|', $self->git, @args
77 or die "failed to run git command";
78 binmode $fh, ':encoding(UTF-8)';
80 my $output = do { local $/ = undef; <$fh> };
87 my($self, $project) = @_;
89 my $dir = blessed($project) && $project->isa('Path::Class::Dir')
91 : $self->git_dir_from_project_name($project);
99 my ($self, $project, @args) = @_;
101 return $self->run_cmd('--git-dir' => $self->project_dir($project)."/.git", @args);
105 my ($self, $project) = @_;
109 $self->get_project_properties(
110 $self->git_dir_from_project_name($project),
115 sub get_project_properties {
116 my ($self, $dir) = @_;
120 $props{description} = $dir->file('description')->slurp;
121 chomp $props{description};
124 if ($props{description} && $props{description} =~ /^Unnamed repository;/) {
125 delete $props{description};
128 ($props{owner} = (getpwuid $dir->stat->uid)[6]) =~ s/,+$//;
130 my $output = $self->run_cmd_in($dir, qw{
131 for-each-ref --format=%(committer)
132 --sort=-committerdate --count=1 refs/heads
135 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
136 my $dt = DateTime->from_epoch(epoch => $epoch);
137 $dt->set_time_zone($tz);
138 $props{last_change} = $dt;
147 my $base = dir($self->repo_dir);
150 my $dh = $base->open;
151 while (my $file = $dh->read) {
152 next if $file =~ /^.{1,2}$/;
154 my $obj = $base->subdir($file);
156 next unless $self->is_git_repo($obj);
158 # XXX Leaky abstraction alert!
159 my $is_bare = !-d $obj->subdir('.git');
161 my $name = (File::Spec->splitdir($obj))[-1];
163 name => ($name . ( $is_bare ? '.git' : '/.git' )),
164 $self->get_project_properties(
165 $is_bare ? $obj : $obj->subdir('.git')
170 return [sort { $a->{name} cmp $b->{name} } @ret];
173 sub git_dir_from_project_name {
174 my ($self, $project) = @_;
176 return dir($self->repo_dir)->subdir($project);
180 my ($self, $project) = @_;
182 my $output = $self->run_cmd_in($self->project, qw/rev-parse --verify HEAD/ );
183 return unless defined $output;
185 my ($head) = $output =~ /^([0-9a-fA-F]{40})$/;
190 my ($self, $project, $rev) = @_;
192 $rev ||= $self->get_head_hash($project);
194 my $output = $self->run_cmd_in($project, qw/ls-tree -z/, $rev);
195 return unless defined $output;
198 for my $line (split /\0/, $output) {
199 my ($mode, $type, $object, $file) = split /\s+/, $line, 4;
212 sub get_object_mode_string {
213 my ($self, $object) = @_;
215 return unless $object && $object->{mode};
216 return mode_to_string($object->{mode});
219 sub get_object_type {
220 my ($self, $project, $object) = @_;
222 my $output = $self->run_cmd_in($project, qw/cat-file -t/, $object);
223 return unless $output;
229 sub get_hash_by_path {
230 my($self, $base, $path, $type) = @_;
234 my $line = $self->run_cmd_in($self->project, 'ls-tree', $base, '--', $path)
237 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
238 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
239 return defined $type && $type ne $2
245 my ($self, $object) = @_;
247 my $type = $self->get_object_type($self->project, $object);
248 die "object `$object' is not a file\n"
249 if (!defined $type || $type ne 'blob');
251 my $output = $self->run_cmd_in($self->project, qw/cat-file -p/, $object);
252 return unless $output;
258 my ($self, $rev) = @_;
261 return ($rev =~ /^([0-9a-fA-F]{40})$/);
265 my ($self, $project, @revs) = @_;
267 croak("Gitalist::Model::Git::diff needs a project and either one or two revisions")
270 || any { !$self->valid_rev($_) } @revs;
272 my $output = $self->run_cmd_in($project, 'diff', @revs);
273 return unless $output;
279 my $formatter = DateTime::Format::Mail->new;
282 my ($self, $output) = @_;
285 my @revs = split /\0/, $output;
287 for my $rev (split /\0/, $output) {
288 for my $line (split /\n/, $rev, 6) {
292 if ($self->valid_rev($line)) {
293 push @ret, {rev => $line};
297 if (my ($key, $value) = $line =~ /^(tree|parent)\s+(.*)$/) {
298 $ret[-1]->{$key} = $value;
302 if (my ($key, $value, $epoch, $tz) = $line =~ /^(author|committer)\s+(.*)\s+(\d+)\s+([+-]\d+)$/) {
303 $ret[-1]->{$key} = $value;
305 $ret[-1]->{ $key . "_datetime" } = DateTime->from_epoch(epoch => $epoch);
306 $ret[-1]->{ $key . "_datetime" }->set_time_zone($tz);
307 $ret[-1]->{ $key . "_datetime" }->set_formatter($formatter);
311 $ret[-1]->{ $key . "_datetime" } = "$epoch $tz";
314 if (my ($name, $email) = $value =~ /^([^<]+)\s+<([^>]+)>$/) {
315 $ret[-1]->{ $key . "_name" } = $name;
316 $ret[-1]->{ $key . "_email" } = $email;
320 $line =~ s/^\n?\s{4}//;
321 $ret[-1]->{longmessage} = $line;
322 $ret[-1]->{message} = (split /\n/, $line, 2)[0];
331 my ($self, $project, %args) = @_;
333 $args{rev} ||= $self->get_head_hash($project);
335 my $output = $self->run_cmd_in($project, 'rev-list',
337 (defined $args{ count } ? "--max-count=$args{count}" : ()),
338 (defined $args{ skip } ? "--skip=$args{skip}" : ()),
343 return unless $output;
345 my @revs = $self->parse_rev_list($output);
351 my ($self, $project, $rev) = @_;
353 return unless $self->valid_rev($rev);
355 return $self->list_revs($project, rev => $rev, count => 1);
359 my ($self, @logargs) = @_;
362 = $self->run_cmd_in($self->project, qw(log -g), @logargs)
363 =~ /(^commit.+?(?:(?=^commit)|(?=\z)))/msg;
367 commit 02526fc15beddf2c64798a947fecdd8d11bf993d
368 Reflog: HEAD@{14} (The Git Server <git@git.dev.venda.com>)
370 Author: Foo Barsby <fbarsby@example.com>
371 Date: Thu Sep 17 12:26:05 2009 +0100
373 Merge branch 'abc123'
378 # XXX Stuff like this makes me want to switch to Git::PurePerl
379 my($sha1, $type, $author, $date)
381 ^ commit \s+ ([0-9a-f]+)$
383 Reflog[ ]message: \s+ (.+?)$ \s+
384 Author: \s+ ([^<]+) <.*?$ \s+
388 pos($_) = index($_, $date) + length $date;
390 # Yeah, I just did that.
392 my($msg) = /\G\s+(\S.*)/sg;
399 # XXX Add DateTime goodness.
407 my ($self, $project) = @_;
409 my $output = $self->run_cmd_in($project, qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
410 return unless $output;
413 for my $line (split /\n/, $output) {
414 my ($rev, $head, $commiter) = split /\0/, $line, 3;
415 $head =~ s!^refs/heads/!!;
417 push @ret, { rev => $rev, name => $head };
419 #FIXME: That isn't the time I'm looking for..
420 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
421 my $dt = DateTime->from_epoch(epoch => $epoch);
422 $dt->set_time_zone($tz);
423 $ret[-1]->{last_change} = $dt;
432 Return a list of refs (e.g branches) for a given sha1.
437 my($self, $sha1) = @_;
439 my $refs = $self->references->{$sha1};
441 return $refs ? @$refs : ();
446 A wrapper for C<git show-ref --dereference>. Based on gitweb's
447 C<git_get_references>.
454 return $self->{references}
455 if $self->{references};
457 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
458 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
459 my $reflist = $self->run_cmd_in($self->project, qw(show-ref --dereference))
463 for(split /\n/, $reflist) {
464 push @{$refs{$1}}, $2
465 if m!^([0-9a-fA-F]{40})\srefs/(.*)$!;
468 return $self->{references} = \%refs;
472 my ($self, $project, $rev) = @_;
474 #FIXME: huge memory consuption
476 return $self->run_cmd_in($project, qw/archive --format=tar/, "--prefix=${project}/", $rev);
481 __PACKAGE__->meta->make_immutable;