1 package Gitalist::Model::Git;
4 use namespace::autoclean;
6 BEGIN { extends 'Catalyst::Model' }
14 use DateTime::Format::Mail;
15 use File::Stat::ModeString;
16 use List::MoreUtils qw/any/;
17 use Scalar::Util qw/blessed/;
25 if (my $config_git = Gitalist->config->{git}) {
26 $git = $config_git if -x $config_git;
30 $git = File::Which::which('git');
35 Could not find a git executable.
36 Please specify the which git executable to use in gitweb.yml
44 has project => (is => 'rw', isa => 'Str');
47 my ($self, $dir) = @_;
49 return -f $dir->file('HEAD') || -f $dir->file('.git/HEAD');
53 my ($self, $project) = @_;
57 $self->get_project_properties(
58 $self->git_dir_from_project_name($project),
63 sub get_project_properties {
64 my ($self, $dir) = @_;
68 $props{description} = $dir->file('description')->slurp;
69 chomp $props{description};
72 if ($props{description} && $props{description} =~ /^Unnamed repository;/) {
73 delete $props{description};
76 ($props{owner} = (getpwuid $dir->stat->uid)[6]) =~ s/,+$//;
78 my $output = $self->run_cmd_in($dir, qw{
79 for-each-ref --format=%(committer)
80 --sort=-committerdate --count=1 refs/heads
83 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
84 my $dt = DateTime->from_epoch(epoch => $epoch);
85 $dt->set_time_zone($tz);
86 $props{last_change} = $dt;
95 my $base = dir(Gitalist->config->{repo_dir});
99 while (my $file = $dh->read) {
100 next if $file =~ /^.{1,2}$/;
102 my $obj = $base->subdir($file);
104 next unless $self->is_git_repo($obj);
106 # XXX Leaky abstraction alert!
107 my $is_bare = !-d $obj->subdir('.git');
109 my $name = (File::Spec->splitdir($obj))[-1];
111 name => ($name . ( $is_bare ? '.git' : '/.git' )),
112 $self->get_project_properties(
113 $is_bare ? $obj : $obj->subdir('.git')
118 return [sort { $a->{name} cmp $b->{name} } @ret];
122 my ($self, @args) = @_;
124 open my $fh, '-|', __PACKAGE__->git_bin, @args
125 or die "failed to run git command";
126 binmode $fh, ':encoding(UTF-8)';
128 print STDERR "RAN - git @_[1..$#_]\n";
130 my $output = do { local $/ = undef; <$fh> };
137 my($self, $project) = @_;
139 my $dir = blessed($project) && $project->isa('Path::Class::Dir')
140 ? $project->stringify
141 : $self->git_dir_from_project_name($project);
149 my ($self, $project, @args) = @_;
151 return $self->run_cmd('--git-dir' => $self->project_dir($project), @args);
154 sub git_dir_from_project_name {
155 my ($self, $project) = @_;
157 return dir(Gitalist->config->{repo_dir})->subdir($project);
161 my ($self, $project) = @_;
163 my $output = $self->run_cmd_in($self->project, qw/rev-parse --verify HEAD/ );
164 return unless defined $output;
166 my ($head) = $output =~ /^([0-9a-fA-F]{40})$/;
171 my ($self, $project, $rev) = @_;
173 $rev ||= $self->get_head_hash($project);
175 my $output = $self->run_cmd_in($project, qw/ls-tree -z/, $rev);
176 return unless defined $output;
179 for my $line (split /\0/, $output) {
180 my ($mode, $type, $object, $file) = split /\s+/, $line, 4;
193 sub get_object_mode_string {
194 my ($self, $object) = @_;
196 return unless $object && $object->{mode};
197 return mode_to_string($object->{mode});
200 sub get_object_type {
201 my ($self, $project, $object) = @_;
203 my $output = $self->run_cmd_in($project, qw/cat-file -t/, $object);
204 return unless $output;
210 sub get_hash_by_path {
211 my($self, $base, $path, $type) = @_;
215 my $line = $self->run_cmd_in($self->project, 'ls-tree', $base, '--', $path)
218 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
219 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
220 return defined $type && $type ne $2
226 my ($self, $object) = @_;
228 my $type = $self->get_object_type($self->project, $object);
229 die "object `$object' is not a file\n"
230 if (!defined $type || $type ne 'blob');
232 my $output = $self->run_cmd_in($self->project, qw/cat-file -p/, $object);
233 return unless $output;
239 my ($self, $rev) = @_;
242 return ($rev =~ /^([0-9a-fA-F]{40})$/);
246 my ($self, $project, @revs) = @_;
248 croak("Gitalist::Model::Git::diff needs a project and either one or two revisions")
251 || any { !$self->valid_rev($_) } @revs;
253 my $output = $self->run_cmd_in($project, 'diff', @revs);
254 return unless $output;
260 my $formatter = DateTime::Format::Mail->new;
263 my ($self, $output) = @_;
266 my @revs = split /\0/, $output;
268 for my $rev (split /\0/, $output) {
269 for my $line (split /\n/, $rev, 6) {
273 if ($self->valid_rev($line)) {
274 push @ret, {rev => $line};
278 if (my ($key, $value) = $line =~ /^(tree|parent)\s+(.*)$/) {
279 $ret[-1]->{$key} = $value;
283 if (my ($key, $value, $epoch, $tz) = $line =~ /^(author|committer)\s+(.*)\s+(\d+)\s+([+-]\d+)$/) {
284 $ret[-1]->{$key} = $value;
286 $ret[-1]->{ $key . "_datetime" } = DateTime->from_epoch(epoch => $epoch);
287 $ret[-1]->{ $key . "_datetime" }->set_time_zone($tz);
288 $ret[-1]->{ $key . "_datetime" }->set_formatter($formatter);
292 $ret[-1]->{ $key . "_datetime" } = "$epoch $tz";
295 if (my ($name, $email) = $value =~ /^([^<]+)\s+<([^>]+)>$/) {
296 $ret[-1]->{ $key . "_name" } = $name;
297 $ret[-1]->{ $key . "_email" } = $email;
301 $line =~ s/^\n?\s{4}//;
302 $ret[-1]->{longmessage} = $line;
303 $ret[-1]->{message} = (split /\n/, $line, 2)[0];
312 my ($self, $project, %args) = @_;
314 $args{rev} ||= $self->get_head_hash($project);
316 my $output = $self->run_cmd_in($project, 'rev-list',
318 (defined $args{ count } ? "--max-count=$args{count}" : ()),
319 (defined $args{ skip } ? "--skip=$args{skip}" : ()),
324 return unless $output;
326 my @revs = $self->parse_rev_list($output);
332 my ($self, $project, $rev) = @_;
334 return unless $self->valid_rev($rev);
336 return $self->list_revs($project, rev => $rev, count => 1);
340 my ($self, @logargs) = @_;
343 = $self->run_cmd_in($self->project, qw(log -g), @logargs)
344 =~ /(^commit.+?(?:(?=^commit)|(?=\z)))/msg;
348 commit 02526fc15beddf2c64798a947fecdd8d11bf993d
349 Reflog: HEAD@{14} (The Git Server <git@git.dev.venda.com>)
351 Author: Iain Loasby <iloasby@rowlf.of-2.uk.venda.com>
352 Date: Thu Sep 17 12:26:05 2009 +0100
354 Merge branch 'rt125181
359 # XXX Stuff like this makes me want to switch to Git::PurePerl
360 my($sha1, $type, $author, $date)
362 ^ commit \s+ ([0-9a-f]+)$
364 Reflog[ ]message: \s+ (.+?)$ \s+
365 Author: \s+ ([^<]+) <.*?$ \s+
369 pos($_) = index($_, $date) + length $date;
371 # Yeah, I just did that.
373 my($msg) = /\G\s+(\S.*)/sg;
380 # XXX Add DateTime goodness.
388 my ($self, $project) = @_;
390 my $output = $self->run_cmd_in($project, qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
391 return unless $output;
394 for my $line (split /\n/, $output) {
395 my ($rev, $head, $commiter) = split /\0/, $line, 3;
396 $head =~ s!^refs/heads/!!;
398 push @ret, { rev => $rev, name => $head };
400 #FIXME: That isn't the time I'm looking for..
401 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
402 my $dt = DateTime->from_epoch(epoch => $epoch);
403 $dt->set_time_zone($tz);
404 $ret[-1]->{last_change} = $dt;
412 my ($self, $project, $rev) = @_;
414 #FIXME: huge memory consuption
416 return $self->run_cmd_in($project, qw/archive --format=tar/, "--prefix=${project}/", $rev);
421 __PACKAGE__->meta->make_immutable;