use Scalar::Util qw/blessed/;
use MooseX::Types::Common::String qw/NonEmptySimpleStr/; # FIXME, use Types::Path::Class and coerce
+=head1 NAME
+
+Gitalist::Model::Git - the model for git interactions
+
+=head1 DESCRIPTION
+
+[enter your description here]
+
+=head1 METHODS
+
+=cut
+
# Should these live in a separate module? Or perhaps extended Regexp::Common?
our $SHA1RE = qr/[0-9a-fA-F]{40}/;
has repo_dir => ( isa => NonEmptySimpleStr, is => 'ro', lazy_build => 1 ); # Fixme - path::class
has git => ( isa => NonEmptySimpleStr, is => 'ro', lazy_build => 1 );
+=head2 BUILD
+
+=cut
+
sub BUILD {
my ($self) = @_;
$self->git; # Cause lazy value build.
lazy => 1,
default => sub {
my($self) = @_;
+ (my $pd = $self->project_dir( $self->project )) =~ s{/\.git$}();
return Git::PurePerl->new(
- directory => $self->project_dir( $self->project )
+ directory => $pd
);
},
);
return Gitalist->config->{repo_dir};
}
+=head2 get_object
+
+A wrapper for the equivalent L<Git::PurePerl> method.
+
+=cut
+
sub get_object {
$_[0]->gpp->get_object($_[1]);
}
+=head2 is_git_repo
+
+Determine whether a given directory (as a L<Path::Class::Dir> object) is a
+C<git> repo.
+
+=cut
+
sub is_git_repo {
my ($self, $dir) = @_;
return -f $dir->file('HEAD') || -f $dir->file('.git/HEAD');
}
+=head2 run_cmd
+
+Call out to the C<git> binary and return a string consisting of the output.
+
+=cut
+
sub run_cmd {
my ($self, @args) = @_;
return $output;
}
+=head2 project_dir
+
+The directory under which the given project will reside i.e C<.git/..>
+
+=cut
+
sub project_dir {
my($self, $project) = @_;
my $dir = blessed($project) && $project->isa('Path::Class::Dir')
? $project->stringify
- : $self->git_dir_from_project_name($project);
+ : $self->dir_from_project_name($project);
- $dir =~ s/\.git$//;
+ $dir .= '/.git'
+ if -f dir($dir)->file('.git/HEAD');
return $dir;
}
+=head2 run_cmd_in
+
+Run a C<git> command in a given project and return the output as a string.
+
+=cut
+
sub run_cmd_in {
my ($self, $project, @args) = @_;
- return $self->run_cmd('--git-dir' => $self->project_dir($project)."/.git", @args);
+ return $self->run_cmd('--git-dir' => $self->project_dir($project), @args);
}
+=head2 command
+
+Run a C<git> command for the project specified in the C<p> parameter and
+return the output as a list of strings corresponding to the lines of output.
+
+=cut
+
sub command {
my($self, @args) = @_;
- my $output = $self->run_cmd('--git-dir' => $self->project_dir($self->project)."/.git", @args);
+ my $output = $self->run_cmd('--git-dir' => $self->project_dir($self->project), @args);
return $output ? split(/\n/, $output) : ();
}
+=head2 project_info
+
+Returns a hash corresponding to a given project's properties. The keys will
+be:
+
+ name
+ description (empty if .git/description is empty/unnamed)
+ owner
+ last_change
+
+=cut
+
sub project_info {
my ($self, $project) = @_;
return {
name => $project,
$self->get_project_properties(
- $self->git_dir_from_project_name($project),
- ),
- };
+ $self->dir_from_project_name($project),
+ ),
+ };
}
+=head2 get_project_properties
+
+Called by C<project_info> to get a project's properties.
+
+=cut
+
sub get_project_properties {
my ($self, $dir) = @_;
my %props;
return %props;
}
+=head2 list_projects
+
+For the C<repo_dir> specified in the config return an array of projects where
+each item will contain the contents of L</project_info>.
+
+=cut
+
sub list_projects {
- my ($self) = @_;
+ my ($self, $dir) = @_;
- my $base = dir($self->repo_dir);
+ my $base = dir($dir || $self->repo_dir);
my @ret;
my $dh = $base->open;
my $name = (File::Spec->splitdir($obj))[-1];
push @ret, {
- name => ($name . ( $is_bare ? '.git' : '/.git' )),
+ name => ($name . ( $is_bare ? '' : '/.git' )),
$self->get_project_properties(
$is_bare ? $obj : $obj->subdir('.git')
),
return [sort { $a->{name} cmp $b->{name} } @ret];
}
-sub git_dir_from_project_name {
+=head2 dir_from_project_name
+
+Get the corresponding directory of a given project.
+
+=cut
+
+sub dir_from_project_name {
my ($self, $project) = @_;
return dir($self->repo_dir)->subdir($project);
}
+=head2 head_hash
+
+Find the C<HEAD> of given (or current) project.
+
+=cut
+
sub head_hash {
my ($self, $project) = @_;
return $head;
}
+=head2 list_tree
+
+For a given tree sha1 return an array describing the tree's contents. Where
+the keys for each item will be:
+
+ mode
+ type
+ object
+ file
+
+=cut
+
sub list_tree {
- my ($self, $project, $rev) = @_;
+ my ($self, $rev, $project) = @_;
+ $project ||= $self->project;
$rev ||= $self->head_hash($project);
my $output = $self->run_cmd_in($project, qw/ls-tree -z/, $rev);
type => $type,
object => $object,
file => $file,
- };
+ };
}
return @ret;
}
+=head2 get_object_mode_string
+
+Provide a string equivalent of an octal mode e.g 0644 eq '-rw-r--r--'.
+
+=cut
+
sub get_object_mode_string {
my ($self, $object) = @_;
return mode_to_string($object->{mode});
}
+=head2 get_object_type
+
+=cut
+
sub get_object_type {
- my ($self, $project, $object) = @_;
+ my ($self, $object, $project) = @_;
- my $output = $self->run_cmd_in($project, qw/cat-file -t/, $object);
+ chomp(my $output = $self->run_cmd_in($project || $self->project, qw/cat-file -t/, $object));
return unless $output;
- chomp $output;
return $output;
}
+=head2 cat_file
+
+Return the contents of a given file.
+
+=cut
+
+sub cat_file {
+ my ($self, $object, $project) = @_;
+
+ my $type = $self->get_object_type($object);
+ die "object `$object' is not a file\n"
+ if (!defined $type || $type ne 'blob');
+
+ my $output = $self->run_cmd_in($project || $self->project, qw/cat-file -p/, $object);
+ return unless $output;
+
+ return $output;
+}
+
+=head2 hash_by_path
+
+For a given sha1 and path find the corresponding hash. Useful for find blobs.
+
+=cut
+
sub hash_by_path {
my($self, $base, $path, $type) = @_;
: $3;
}
-sub cat_file {
- my ($self, $object) = @_;
-
- my $type = $self->get_object_type($self->project, $object);
- die "object `$object' is not a file\n"
- if (!defined $type || $type ne 'blob');
+=head2 valid_rev
- my $output = $self->run_cmd_in($self->project, qw/cat-file -p/, $object);
- return unless $output;
+Check whether a given rev is valid i.e looks like a sha1.
- return $output;
-}
+=cut
sub valid_rev {
my ($self, $rev) = @_;
return ($rev =~ /^($SHA1RE)$/);
}
+=head2 diff
+
+
+
+=cut
+
sub diff {
- my ($self, $project, @revs) = @_;
+ my ($self, @revs, $project) = @_;
croak("Gitalist::Model::Git::diff needs a project and either one or two revisions")
if scalar @revs < 1
|| scalar @revs > 2
|| any { !$self->valid_rev($_) } @revs;
- my $output = $self->run_cmd_in($project, 'diff', @revs);
+ my $output = $self->run_cmd_in($project || $self->project, 'diff', @revs);
return unless $output;
return $output;
{
my $formatter = DateTime::Format::Mail->new;
+=head2 parse_rev_list
+
+Given the output of the C<rev-list> command return a list of hashes.
+
+=cut
+
sub parse_rev_list {
my ($self, $output) = @_;
my @ret;
}
}
+=head2 list_revs
+
+Calls the C<rev-list> command (a low-level from of C<log>) and returns an
+array of hashes.
+
+=cut
+
sub list_revs {
- my ($self, $project, %args) = @_;
+ my ($self, %args) = @_;
- $args{rev} ||= $self->head_hash($project);
+ $args{rev} ||= $self->head_hash($args{project});
- my $output = $self->run_cmd_in($project, 'rev-list',
+ my $output = $self->run_cmd_in($args{project} || $self->project, 'rev-list',
'--header',
(defined $args{ count } ? "--max-count=$args{count}" : ()),
- (defined $args{ skip } ? "--skip=$args{skip}" : ()),
+ (defined $args{ skip } ? "--skip=$args{skip}" : ()),
$args{rev},
'--',
- ($args{file} || ()),
- );
+ ($args{file} ? $args{file} : ()),
+ );
return unless $output;
my @revs = $self->parse_rev_list($output);
return \@revs;
}
+=head2 rev_info
+
+Get a single piece of revision information for a given sha1.
+
+=cut
+
sub rev_info {
- my ($self, $project, $rev) = @_;
+ my($self, $rev, $project) = @_;
return unless $self->valid_rev($rev);
- return $self->list_revs($project, rev => $rev, count => 1);
+ return $self->list_revs(
+ rev => $rev, count => 1,
+ ( $project ? (project => $project) : () )
+ );
}
+=head2 reflog
+
+Calls the C<reflog> command and returns a list of hashes.
+
+=cut
+
sub reflog {
my ($self, @logargs) = @_;
Reflog[ ]message: \s+ (.+?)$ \s+
Author: \s+ ([^<]+) <.*?$ \s+
Date: \s+ (.+?)$
-}xms;
+ }xms;
pos($_) = index($_, $date) + length $date;
} @entries;
}
+=head2 get_heads
+
+Returns an array of hashes representing the heads (aka branches) for the
+given, or current, project.
+
+=cut
+
sub get_heads {
my ($self, $project) = @_;
- my $output = $self->run_cmd_in($project, qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
+ my $output = $self->run_cmd_in($project || $self->project, qw/for-each-ref --sort=-committerdate /, '--format=%(objectname)%00%(refname)%00%(committer)', 'refs/heads');
return unless $output;
my @ret;
=head2 refs_for
-Return a list of refs (e.g branches) for a given sha1.
+For a given sha1 check which branches currently point at it.
=cut
return $refs ? @$refs : ();
}
-=head2
+=head2 references
A wrapper for C<git show-ref --dereference>. Based on gitweb's
C<git_get_references>.
use List::MoreUtils qw(zip);
# XXX Hrm, getting called twice, not sure why.
+=head2 diff_tree
+
+Given a L<Git::PurePerl> commit object return a list of hashes corresponding
+to the C<diff-tree> output.
+
+=cut
+
sub diff_tree {
my($self, $commit) = @_;
return @difftree;
}
-sub archive {
- my ($self, $project, $rev) = @_;
-
- #FIXME: huge memory consuption
- #TODO: compression
- return $self->run_cmd_in($project, qw/archive --format=tar/, "--prefix=${project}/", $rev);
-}
-
1;
__PACKAGE__->meta->make_immutable;
my $repoEmpty = Path::Class::Dir->new('t/lib/repositories/empty.git');
ok( ! $m->is_git_repo( $repoEmpty ), 'is_git_repo is false for empty dir' );
-my $projectList = $m->list_projects;
+my $projectList = $m->list_projects('t/lib/repositories');
ok( scalar @{$projectList} == 2, 'list_projects returns an array with the correct number of members' );
-ok( $projectList->[0]->{name} eq 'bare.git', 'list_projects has correct name for "bare.git" repo' );
+is( $projectList->[0]->{name}, 'bare.git', 'list_projects has correct name for "bare.git" repo' );
#ok( $projectList->[1]->{name} eq 'working/.git', 'list_projects has correct name for "working" repo' );
# Liberally borrowed from rafl's gitweb
my $repo = 'repo1';
-like($m->get_head_hash($repo), qr/^([0-9a-fA-F]{40})$/, 'get_head_hash');
+like($m->head_hash($repo), qr/^([0-9a-fA-F]{40})$/, 'get_head_hash');
{
- my @tree = $m->list_tree($repo, '3bc0634310b9c62222bb0e724c11ffdfb297b4ac');
+ my @tree = $m->list_tree('3bc0634310b9c62222bb0e724c11ffdfb297b4ac', $repo);
is(scalar @tree, 1);
is_deeply($tree[0], {
is($m->get_object_mode_string($tree[0]), '-rw-r--r--');
}
-is($m->get_object_type($repo, '729a7c3f6ba5453b42d16a43692205f67fb23bc1'), 'tree');
-is($m->get_object_type($repo, '257cc5642cb1a054f08cc83f2d943e56fd3ebe99'), 'blob');
-is($m->get_object_type($repo, '5716ca5987cbf97d6bb54920bea6adde242d87e6'), 'blob');
+is($m->get_object_type('729a7c3f6ba5453b42d16a43692205f67fb23bc1', $repo), 'tree');
+is($m->get_object_type('257cc5642cb1a054f08cc83f2d943e56fd3ebe99', $repo), 'blob');
+is($m->get_object_type('5716ca5987cbf97d6bb54920bea6adde242d87e6', $repo), 'blob');
-is($m->cat_file($repo, '257cc5642cb1a054f08cc83f2d943e56fd3ebe99'), "foo\n");
-is($m->cat_file($repo, '5716ca5987cbf97d6bb54920bea6adde242d87e6'), "bar\n");
+is($m->cat_file('257cc5642cb1a054f08cc83f2d943e56fd3ebe99', $repo), "foo\n");
+is($m->cat_file('5716ca5987cbf97d6bb54920bea6adde242d87e6', $repo), "bar\n");
-is($m->diff($repo, '3bc0634310b9c62222bb0e724c11ffdfb297b4ac', '3f7567c7bdf7e7ebf410926493b92d398333116e'), <<EOD);
+is($m->diff('3bc0634310b9c62222bb0e724c11ffdfb297b4ac', '3f7567c7bdf7e7ebf410926493b92d398333116e', $repo), <<EOD);
diff --git a/file1 b/file1
index 257cc56..5716ca5 100644
--- a/file1
-foo
+bar
EOD
-
-use Data::Dumper;
-warn( Dumper( $m->list_revs($repo) ));