1 package Gitalist::Model::Git;
4 use MooseX::Types::Common::String qw/NonEmptySimpleStr/; # FIXME, use Types::Path::Class and coerce
5 use namespace::autoclean;
7 BEGIN { extends 'Catalyst::Model' }
13 use DateTime::Format::Mail;
14 use File::Stat::ModeString;
15 use List::MoreUtils qw/any/;
20 $self->git; # Cause lazy value build.
23 has git => ( isa => NonEmptySimpleStr, is => 'ro', lazy_build => 1 );
26 my $git = File::Which::which('git');
30 Could not find a git executable.
31 Please specify the which git executable to use in gitweb.yml
39 my ($self, $dir) = @_;
41 return -f $dir->file('HEAD') || -f $dir->file('.git/HEAD');
45 my ($self, $project) = @_;
49 $self->get_project_properties(
50 $self->git_dir_from_project_name($project),
55 sub get_project_properties {
56 my ($self, $dir) = @_;
60 $props{description} = $dir->file('description')->slurp;
61 chomp $props{description};
64 if ($props{description} && $props{description} =~ /^Unnamed repository;/) {
65 delete $props{description};
68 $props{owner} = (getpwuid $dir->stat->uid)[6];
70 my $output = $self->run_cmd_in($dir, qw{
71 for-each-ref --format=%(committer)
72 --sort=-committerdate --count=1 refs/heads
75 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
76 my $dt = DateTime->from_epoch(epoch => $epoch);
77 $dt->set_time_zone($tz);
78 $props{last_change} = $dt;
84 has repo_dir => ( isa => NonEmptySimpleStr, required => 1, is => 'ro' ); # Fixme - path::class
89 my $base = dir($self->repo_dir);
93 while (my $file = $dh->read) {
94 next if $file =~ /^.{1,2}$/;
96 my $obj = $base->subdir($file);
98 next unless $self->is_git_repo($obj);
99 # XXX Leaky abstraction alert!
100 my $is_bare = !-d $obj->subdir('.git');
102 my $name = (File::Spec->splitdir($obj))[-1];
104 name => ($name . ( $is_bare ? '' : '/.git' )),
105 $self->get_project_properties(
106 $is_bare ? $obj : $obj->subdir('.git')
111 return [sort { $a->{name} cmp $b->{name} } @ret];
115 my ($self, @args) = @_;
117 open my $fh, '-|', $self->git, @args
118 or die "failed to run git command";
119 binmode $fh, ':encoding(UTF-8)';
121 my $output = do { local $/ = undef; <$fh> };
128 my ($self, $project, @args) = @_;
131 if (blessed($project) && $project->isa('Path::Class::Dir')) {
132 $path = $project->stringify;
135 $path = $self->git_dir_from_project_name($project);
137 return $self->run_cmd('--git-dir' => $path, @args);
140 sub git_dir_from_project_name {
141 my ($self, $project) = @_;
143 return dir($self->repo_dir)->subdir($project);
147 my ($self, $project) = @_;
149 my $output = $self->run_cmd_in($project, qw/rev-parse --verify HEAD/ );
150 return unless defined $output;
152 my ($head) = $output =~ /^([0-9a-fA-F]{40})$/;
157 my ($self, $project, $rev) = @_;
159 $rev ||= $self->get_head_hash($project);
161 my $output = $self->run_cmd_in($project, qw/ls-tree -z/, $rev);
162 return unless defined $output;
165 for my $line (split /\0/, $output) {
166 my ($mode, $type, $object, $file) = split /\s+/, $line, 4;
179 sub get_object_mode_string {
180 my ($self, $object) = @_;
182 return unless $object && $object->{mode};
183 return mode_to_string($object->{mode});
186 sub get_object_type {
187 my ($self, $project, $object) = @_;
189 my $output = $self->run_cmd_in($project, qw/cat-file -t/, $object);
190 return unless $output;
197 my ($self, $project, $object) = @_;
199 my $type = $self->get_object_type($project, $object);
200 die "object `$object' is not a file\n"
201 if (!defined $type || $type ne 'blob');
203 my $output = $self->run_cmd_in($project, qw/cat-file -p/, $object);
204 return unless $output;
210 my ($self, $rev) = @_;
213 return ($rev =~ /^([0-9a-fA-F]{40})$/);
217 my ($self, $project, @revs) = @_;
219 croak("Gitalist::Model::Git::diff needs a project and either one or two revisions")
222 || any { !$self->valid_rev($_) } @revs;
224 my $output = $self->run_cmd_in($project, 'diff', @revs);
225 return unless $output;
231 my $formatter = DateTime::Format::Mail->new;
234 my ($self, $output) = @_;
237 my @revs = split /\0/, $output;
239 for my $rev (split /\0/, $output) {
240 for my $line (split /\n/, $rev, 6) {
244 if ($self->valid_rev($line)) {
245 push @ret, {rev => $line};
249 if (my ($key, $value) = $line =~ /^(tree|parent)\s+(.*)$/) {
250 $ret[-1]->{$key} = $value;
254 if (my ($key, $value, $epoch, $tz) = $line =~ /^(author|committer)\s+(.*)\s+(\d+)\s+([+-]\d+)$/) {
255 $ret[-1]->{$key} = $value;
257 $ret[-1]->{ $key . "_datetime" } = DateTime->from_epoch(epoch => $epoch);
258 $ret[-1]->{ $key . "_datetime" }->set_time_zone($tz);
259 $ret[-1]->{ $key . "_datetime" }->set_formatter($formatter);
263 $ret[-1]->{ $key . "_datetime" } = "$epoch $tz";
266 if (my ($name, $email) = $value =~ /^([^<]+)\s+<([^>]+)>$/) {
267 $ret[-1]->{ $key . "_name" } = $name;
268 $ret[-1]->{ $key . "_email" } = $email;
272 $line =~ s/^\n?\s{4}//;
273 $ret[-1]->{longmessage} = $line;
274 $ret[-1]->{message} = (split /\n/, $line, 2)[0];
283 my ($self, $project, %args) = @_;
285 $args{rev} ||= $self->get_head_hash($project);
287 my $output = $self->run_cmd_in($project, 'rev-list',
289 (defined $args{ count } ? "--max-count=$args{count}" : ()),
290 (defined $args{ skip } ? "--skip=$args{skip}" : ()),
295 return unless $output;
297 my @revs = $self->parse_rev_list($output);
303 my ($self, $project, $rev) = @_;
305 return unless $self->valid_rev($rev);
307 return $self->list_revs($project, rev => $rev, count => 1);
311 my ($self, $project) = @_;
313 my $output = $self->run_cmd_in($project, qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
314 return unless $output;
317 for my $line (split /\n/, $output) {
318 my ($rev, $head, $commiter) = split /\0/, $line, 3;
319 $head =~ s!^refs/heads/!!;
321 push @ret, { rev => $rev, name => $head };
323 #FIXME: That isn't the time I'm looking for..
324 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
325 my $dt = DateTime->from_epoch(epoch => $epoch);
326 $dt->set_time_zone($tz);
327 $ret[-1]->{last_change} = $dt;
335 my ($self, $project, $rev) = @_;
337 #FIXME: huge memory consuption
339 return $self->run_cmd_in($project, qw/archive --format=tar/, "--prefix=${project}/", $rev);
344 __PACKAGE__->meta->make_immutable;