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/;
17 use Gitalist::Util qw(to_utf8);
20 use CGI::Util qw(unescape);
29 if (my $config_git = Gitalist->config->{git}) {
30 $git = $config_git if -x $config_git;
34 $git = File::Which::which('git');
39 Could not find a git executable.
40 Please specify the which git executable to use in gitweb.yml
49 my ($self, $dir) = @_;
51 #FIXME: Only handles bare repos. Is that enough?
52 return -f $dir->file('HEAD');
56 my ($self, $project) = @_;
60 $self->get_project_properties(
61 $self->git_dir_from_project_name($project),
66 sub get_project_properties {
67 my ($self, $dir) = @_;
71 $props{description} = $dir->file('description')->slurp;
72 chomp $props{description};
75 if ($props{description} =~ /^Unnamed repository;/) {
76 delete $props{description};
79 $props{owner} = (getpwuid $dir->stat->uid)[6];
81 my $output = $self->run_cmd_in($dir, qw{
82 for-each-ref --format=%(committer)
83 --sort=-committerdate --count=1 refs/heads
86 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
87 my $dt = DateTime->from_epoch(epoch => $epoch);
88 $dt->set_time_zone($tz);
89 $props{last_change} = $dt;
98 my $base = dir(Gitalist->config->{repo_dir});
101 my $dh = $base->open;
102 while (my $file = $dh->read) {
103 next if $file =~ /^.{1,2}$/;
105 my $obj = $base->subdir($file);
107 next unless $self->is_git_repo($obj);
110 name => ($obj->dir_list)[-1],
111 $self->get_project_properties($obj),
119 my ($self, @args) = @_;
121 open my $fh, '-|', __PACKAGE__->git, @args
122 or die "failed to run git command";
123 binmode $fh, ':encoding(UTF-8)';
125 my $output = do { local $/ = undef; <$fh> };
132 my ($self, $project, @args) = @_;
135 if (blessed($project) && $project->isa('Path::Class::Dir')) {
136 $path = $project->stringify;
139 $path = $self->git_dir_from_project_name($project);
141 return $self->run_cmd('--git-dir' => $path, @args);
144 sub git_dir_from_project_name {
145 my ($self, $project) = @_;
147 return dir(Gitalist->config->{repo_dir})->subdir($project);
151 my ($self, $project) = @_;
153 my $output = $self->run_cmd_in($project, qw/rev-parse --verify HEAD/ );
154 return unless defined $output;
156 my ($head) = $output =~ /^([0-9a-fA-F]{40})$/;
161 my ($self, $project, $rev) = @_;
163 $rev ||= $self->get_head_hash($project);
165 my $output = $self->run_cmd_in($project, qw/ls-tree -z/, $rev);
166 return unless defined $output;
169 for my $line (split /\0/, $output) {
170 my ($mode, $type, $object, $file) = split /\s+/, $line, 4;
183 sub get_object_mode_string {
184 my ($self, $object) = @_;
186 return unless $object && $object->{mode};
187 return mode_to_string($object->{mode});
190 sub get_object_type {
191 my ($self, $project, $object) = @_;
193 my $output = $self->run_cmd_in($project, qw/cat-file -t/, $object);
194 return unless $output;
201 my ($self, $project, $object) = @_;
203 my $type = $self->get_object_type($project, $object);
204 die "object `$object' is not a file\n"
205 if (!defined $type || $type ne 'blob');
207 my $output = $self->run_cmd_in($project, qw/cat-file -p/, $object);
208 return unless $output;
214 my ($self, $rev) = @_;
217 return ($rev =~ /^([0-9a-fA-F]{40})$/);
221 my ($self, $project, @revs) = @_;
223 croak("Gitalist::Model::Git::diff needs a project and either one or two revisions")
226 || any { !$self->valid_rev($_) } @revs;
228 my $output = $self->run_cmd_in($project, 'diff', @revs);
229 return unless $output;
235 my $formatter = DateTime::Format::Mail->new;
238 my ($self, $output) = @_;
241 my @revs = split /\0/, $output;
243 for my $rev (split /\0/, $output) {
244 for my $line (split /\n/, $rev, 6) {
248 if ($self->valid_rev($line)) {
249 push @ret, {rev => $line};
253 if (my ($key, $value) = $line =~ /^(tree|parent)\s+(.*)$/) {
254 $ret[-1]->{$key} = $value;
258 if (my ($key, $value, $epoch, $tz) = $line =~ /^(author|committer)\s+(.*)\s+(\d+)\s+([+-]\d+)$/) {
259 $ret[-1]->{$key} = $value;
261 $ret[-1]->{ $key . "_datetime" } = DateTime->from_epoch(epoch => $epoch);
262 $ret[-1]->{ $key . "_datetime" }->set_time_zone($tz);
263 $ret[-1]->{ $key . "_datetime" }->set_formatter($formatter);
267 $ret[-1]->{ $key . "_datetime" } = "$epoch $tz";
270 if (my ($name, $email) = $value =~ /^([^<]+)\s+<([^>]+)>$/) {
271 $ret[-1]->{ $key . "_name" } = $name;
272 $ret[-1]->{ $key . "_email" } = $email;
276 $line =~ s/^\n?\s{4}//;
277 $ret[-1]->{longmessage} = $line;
278 $ret[-1]->{message} = (split /\n/, $line, 2)[0];
287 my ($self, $project, %args) = @_;
289 $args{rev} ||= $self->get_head_hash($project);
291 my $output = $self->run_cmd_in($project, 'rev-list',
293 (defined $args{ count } ? "--max-count=$args{count}" : ()),
294 (defined $args{ skip } ? "--skip=$args{skip}" : ()),
299 return unless $output;
301 my @revs = $self->parse_rev_list($output);
307 my ($self, $project, $rev) = @_;
309 return unless $self->valid_rev($rev);
311 return $self->list_revs($project, rev => $rev, count => 1);
315 my ($self, $project) = @_;
317 my $output = $self->run_cmd_in($project, qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
318 return unless $output;
321 for my $line (split /\n/, $output) {
322 my ($rev, $head, $commiter) = split /\0/, $line, 3;
323 $head =~ s!^refs/heads/!!;
325 push @ret, { rev => $rev, name => $head };
327 #FIXME: That isn't the time I'm looking for..
328 if (my ($epoch, $tz) = $output =~ /\s(\d+)\s+([+-]\d+)$/) {
329 my $dt = DateTime->from_epoch(epoch => $epoch);
330 $dt->set_time_zone($tz);
331 $ret[-1]->{last_change} = $dt;
339 my ($self, $project, $rev) = @_;
341 #FIXME: huge memory consuption
343 return $self->run_cmd_in($project, qw/archive --format=tar/, "--prefix=${project}/", $rev);
348 # checking HEAD file with -e is fragile if the repository was
349 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
351 sub check_head_link {
353 my $headfile = "$dir/HEAD";
354 return ((-e $headfile) ||
355 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
358 sub check_export_ok {
360 my($export_ok, $export_auth_hook) = @{Gitalist->config}{qw(export_ok export_auth_hook)};
361 return (check_head_link($dir) &&
362 (!$export_ok || -e "$dir/$export_ok") &&
363 (!$export_auth_hook || $export_auth_hook->($dir)));
367 my($self, $filter) = @_;
371 $filter =~ s/\.git$//;
373 my $projects_list = Gitalist->config->{projectroot};
374 if (-d $projects_list) {
375 # search in directory
376 my $dir = $projects_list . ($filter ? "/$filter" : '');
377 # remove the trailing "/"
379 my $pfxlen = length("$dir");
380 my $pfxdepth = ($dir =~ tr!/!!);
383 follow_fast => 1, # follow symbolic links
384 follow_skip => 2, # ignore duplicates
385 dangling_symlinks => 0, # ignore dangling symlinks, silently
387 # skip project-list toplevel, if we get it.
388 return if (m!^[/.]$!);
389 # only directories can be git repositories
390 return unless (-d $_);
391 # don't traverse too deep (Find is super slow on os x)
392 if (($File::Find::name =~ tr!/!!) - $pfxdepth > Gitalist->config->{project_maxdepth}) {
393 $File::Find::prune = 1;
397 my $subdir = substr($File::Find::name, $pfxlen + 1);
398 # we check related file in $projectroot
399 my $path = ($filter ? "$filter/" : '') . $subdir;
400 if (check_export_ok("$projects_list/$path")) {
401 push @list, { path => $path };
402 $File::Find::prune = 1;
407 } elsif (-f $projects_list) {
408 # read from file(url-encoded):
409 # 'git%2Fgit.git Linus+Torvalds'
410 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
411 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
413 open my ($fd), $projects_list or return;
415 while (my $line = <$fd>) {
417 my ($path, $owner) = split ' ', $line;
418 $path = unescape($path);
419 $owner = unescape($owner);
420 if (!defined $path) {
425 my $pfx = substr($path, 0, length($filter));
426 if ($pfx ne $filter) {
429 my $sfx = substr($path, length($filter));
430 if ($sfx !~ /^\/.*\.git$/) {
434 if (check_export_ok("$projects_list/$path")) {
437 owner => to_utf8($owner),
440 (my $forks_path = $path) =~ s/\.git$//;
441 $paths{$forks_path}++;
451 __PACKAGE__->meta->make_immutable;