1 package Gitalist::Model::Git;
4 use namespace::autoclean;
6 BEGIN { extends 'Catalyst::Model' }
12 use DateTime::Format::Mail;
13 use File::Stat::ModeString;
14 use List::MoreUtils qw/any/;
15 use Scalar::Util qw/blessed/;
23 if (my $config_git = Gitalist->config->{git}) {
24 $git = $config_git if -x $config_git;
28 $git = File::Which::which('git');
33 Could not find a git executable.
34 Please specify the which git executable to use in gitweb.yml
43 my ($self, $dir) = @_;
45 #FIXME: Only handles bare repos. Is that enough?
46 return -f $dir->file('HEAD') or -f $dir->file('.git/HEAD');
50 my ($self, $project) = @_;
54 $self->get_project_properties(
55 $self->git_dir_from_project_name($project),
60 sub get_project_properties {
61 my ($self, $dir) = @_;
65 $props{description} = $dir->file('description')->slurp;
66 chomp $props{description};
69 if ($props{description} && $props{description} =~ /^Unnamed repository;/) {
70 delete $props{description};
73 #Carp::cluck "dir is: $dir";
74 $props{owner} = (getpwuid $dir->stat->uid)[6];
76 my $output = $self->run_cmd_in($dir, qw{
77 for-each-ref --format=%(committer)
78 --sort=-committerdate --count=1 refs/heads
81 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
82 my $dt = DateTime->from_epoch(epoch => $epoch);
83 $dt->set_time_zone($tz);
84 $props{last_change} = $dt;
93 my $base = dir(Gitalist->config->{repo_dir});
97 while (my $file = $dh->read) {
98 next if $file =~ /^.{1,2}$/;
100 my $obj = $base->subdir($file);
102 next unless $self->is_git_repo($obj);
105 name => ($obj->dir_list)[-1],
106 $self->get_project_properties($obj),
114 my ($self, @args) = @_;
116 open my $fh, '-|', __PACKAGE__->git, @args
117 or die "failed to run git command";
118 binmode $fh, ':encoding(UTF-8)';
120 my $output = do { local $/ = undef; <$fh> };
127 my ($self, $project, @args) = @_;
130 if (blessed($project) && $project->isa('Path::Class::Dir')) {
131 $path = $project->stringify;
134 $path = $self->git_dir_from_project_name($project);
136 return $self->run_cmd('--git-dir' => $path, @args);
139 sub git_dir_from_project_name {
140 my ($self, $project) = @_;
142 warn 'er, dir - '.dir(Gitalist->config->{repo_dir});
143 warn 'er, subdir - '.dir(Gitalist->config->{repo_dir})->subdir($project);
144 return dir(Gitalist->config->{repo_dir})->subdir($project);
148 my ($self, $project) = @_;
150 my $output = $self->run_cmd_in($project, qw/rev-parse --verify HEAD/ );
151 return unless defined $output;
153 my ($head) = $output =~ /^([0-9a-fA-F]{40})$/;
158 my ($self, $project, $rev) = @_;
160 $rev ||= $self->get_head_hash($project);
162 my $output = $self->run_cmd_in($project, qw/ls-tree -z/, $rev);
163 return unless defined $output;
166 for my $line (split /\0/, $output) {
167 my ($mode, $type, $object, $file) = split /\s+/, $line, 4;
180 sub get_object_mode_string {
181 my ($self, $object) = @_;
183 return unless $object && $object->{mode};
184 return mode_to_string($object->{mode});
187 sub get_object_type {
188 my ($self, $project, $object) = @_;
190 my $output = $self->run_cmd_in($project, qw/cat-file -t/, $object);
191 return unless $output;
198 my ($self, $project, $object) = @_;
200 my $type = $self->get_object_type($project, $object);
201 die "object `$object' is not a file\n"
202 if (!defined $type || $type ne 'blob');
204 my $output = $self->run_cmd_in($project, qw/cat-file -p/, $object);
205 return unless $output;
211 my ($self, $rev) = @_;
214 return ($rev =~ /^([0-9a-fA-F]{40})$/);
218 my ($self, $project, @revs) = @_;
220 croak("Gitalist::Model::Git::diff needs a project and either one or two revisions")
223 || any { !$self->valid_rev($_) } @revs;
225 my $output = $self->run_cmd_in($project, 'diff', @revs);
226 return unless $output;
232 my $formatter = DateTime::Format::Mail->new;
235 my ($self, $output) = @_;
238 my @revs = split /\0/, $output;
240 for my $rev (split /\0/, $output) {
241 for my $line (split /\n/, $rev, 6) {
245 if ($self->valid_rev($line)) {
246 push @ret, {rev => $line};
250 if (my ($key, $value) = $line =~ /^(tree|parent)\s+(.*)$/) {
251 $ret[-1]->{$key} = $value;
255 if (my ($key, $value, $epoch, $tz) = $line =~ /^(author|committer)\s+(.*)\s+(\d+)\s+([+-]\d+)$/) {
256 $ret[-1]->{$key} = $value;
258 $ret[-1]->{ $key . "_datetime" } = DateTime->from_epoch(epoch => $epoch);
259 $ret[-1]->{ $key . "_datetime" }->set_time_zone($tz);
260 $ret[-1]->{ $key . "_datetime" }->set_formatter($formatter);
264 $ret[-1]->{ $key . "_datetime" } = "$epoch $tz";
267 if (my ($name, $email) = $value =~ /^([^<]+)\s+<([^>]+)>$/) {
268 $ret[-1]->{ $key . "_name" } = $name;
269 $ret[-1]->{ $key . "_email" } = $email;
273 $line =~ s/^\n?\s{4}//;
274 $ret[-1]->{longmessage} = $line;
275 $ret[-1]->{message} = (split /\n/, $line, 2)[0];
284 my ($self, $project, %args) = @_;
286 $args{rev} ||= $self->get_head_hash($project);
288 my $output = $self->run_cmd_in($project, 'rev-list',
290 (defined $args{ count } ? "--max-count=$args{count}" : ()),
291 (defined $args{ skip } ? "--skip=$args{skip}" : ()),
296 return unless $output;
298 my @revs = $self->parse_rev_list($output);
304 my ($self, $project, $rev) = @_;
306 return unless $self->valid_rev($rev);
308 return $self->list_revs($project, rev => $rev, count => 1);
312 my ($self, $project) = @_;
314 my $output = $self->run_cmd_in($project, qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
315 return unless $output;
318 for my $line (split /\n/, $output) {
319 my ($rev, $head, $commiter) = split /\0/, $line, 3;
320 $head =~ s!^refs/heads/!!;
322 push @ret, { rev => $rev, name => $head };
324 #FIXME: That isn't the time I'm looking for..
325 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
326 my $dt = DateTime->from_epoch(epoch => $epoch);
327 $dt->set_time_zone($tz);
328 $ret[-1]->{last_change} = $dt;
336 my ($self, $project, $rev) = @_;
338 #FIXME: huge memory consuption
340 return $self->run_cmd_in($project, qw/archive --format=tar/, "--prefix=${project}/", $rev);
345 __PACKAGE__->meta->make_immutable;