use inc::Module::Install;
use Module::Install::AuthorRequires;
use Module::Install::AuthorTests;
+use Module::Install::ReadmeFromPod;
name 'Gitalist';
all_from 'lib/Gitalist.pm';
+readme_from 'lib/Gitalist.pm';
requires 'Catalyst::Runtime' => '5.80003';
requires 'Catalyst::Plugin::ConfigLoader';
requires 'Config::General'; # This should reflect the config file format you've chosen
# See Catalyst::Plugin::ConfigLoader for supported formats
requires 'MooseX::Types::Common';
+requires 'MooseX::Types::Path::Class';
+requires 'MooseX::Types';
requires 'File::Find::Rule';
requires 'File::Stat::ModeString';
-requires 'File::Slurp';
requires 'DateTime::Format::Mail';
requires 'IO::Capture::Stdout';
requires 'File::Which';
+requires 'aliased';
requires 'CGI';
requires 'DateTime';
requires 'Git::PurePerl'; # Note - need the git version in broquaint's fork
catalyst;
author_tests 't/author';
-
install_script glob('script/*.pl');
auto_install;
WriteAll;
-Gitalist - a transitional project to convert gitweb.cgi to a Catalyst app.
+NAME
+ Gitalist - Catalyst based application
-The idea behind this project is to move gitweb.cgi away from a single
-monolithic CGI script and into a modern Catalyst app. Fortunately this is not
-as daunting as it might seem at first as gitweb.cgi follows an MVC type
-structure. Once gitweb.cgi has been suitably Catalysed then it can move from
-being "this was once gitweb.cgi" to a project of its own (hence the
-"transitional" in the description).
+SYNOPSIS
+ script/gitalist_server.pl
-As it stands Gitalist is very much in its infancy and hasn't got far
-beyond a layout template and a single controller.
+DESCRIPTION
+ [enter your description here]
-USAGE
+SEE ALSO
+ Gitalist::Controller::Root, Catalyst
-To get Gitalist up and running just set projectroot & repo_dir (one in
-the same unfortunately at this point) in gitalist.conf to point to the
-directory that contains your repositories. With that done just run:
+AUTHORS AND COPYRIGHT
+ Catalyst application:
+ (C) 2009 Venda Ltd and Dan Brook <dbrook@venda.com>
- perl script/gitalist_server.pl
+ Original gitweb.cgi from which this was derived:
+ (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
+ (C) 2005, Christian Gierke
-DEPENDENCIES
+LICENSE
+ FIXME - Is this going to be GPLv2 as per gitweb? If so this is broken..
- Catalyst
- IO::Capture::Stdout
- Catalyst::View::ContentNegotiation::XHTML
- Template::Plugin::Cycle
- DateTime
- Path::Class
- File::Find::Rule;
- DateTime::Format::Mail
- File::Stat::ModeString
- List::MoreUtils
- MooseX::Types::Common
- # Probably others ...
+ This library is free software. You can redistribute it and/or modify it
+ under the same terms as Perl itself.
-COPYRIGHT AND LICENCE
-
-Copyright (C) 2009 Venda Ltd
-
-This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself, either Perl version 5.8.8 or,
-at your option, any later version of Perl 5 you may have available.
-
-gitweb.pm is based on the gitweb.perl from git-1.6.3.3
-(C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
-(C) 2005, Christian Gierke
<Model::Git>
#git /path/to/git
- projectroot __path_to(../)__
repo_dir __path_to(../)__
</Model::Git>
sub summary : Local {
my ( $self, $c ) = @_;
-
+ $c->stash(current_model => 'GitRepos');
+ warn("project is " . $c->stash->{project});
+ my $project;
+ eval {
+ $project = $c->model()->project($c->stash->{project});
+ };
+ if ($@) {
+ $c->detach('error_404');
+ }
my $commit = $self->_get_commit($c);
$c->stash(
commit => $commit,
- info => $c->model()->project_info($c->model()->project),
- log_lines => [$c->model()->list_revs(
- sha1 => $commit->sha1, count => Gitalist->config->{paging}{summary}
+ info => $project->info,
+ log_lines => [$project->list_revs(
+ sha1 => $commit->sha1,
+ count => Gitalist->config->{paging}{summary} || 50
)],
- refs => $c->model()->references,
- heads => [$c->model()->heads],
+ refs => $project->references,
+ heads => [$project->heads],
action => 'summary',
- );
+);
}
=head2 heads
}
}
+sub error_404 :Private {
+ my ($self, $c) = @_;
+ $c->response->status(404);
+ $c->stash(
+ title => 'Page not found',
+ content => 'Page not found',
+ )
+}
+
sub age_string {
my $age = shift;
my $age_str;
use MooseX::Declare;
+use Moose::Autobox;
class Gitalist::Git::Object {
use MooseX::Types::Moose qw/Str Int/;
+ use MooseX::Types::Common::String qw/NonEmptySimpleStr/;
use File::Stat::ModeString qw/mode_to_string/;
# project and sha1 are required initargs
has project => ( isa => 'Gitalist::Git::Project',
required => 1,
is => 'ro',
+ weak_ref => 1,
handles => [ 'run_cmd' ],
);
- has sha1 => ( isa => Str,
+ has sha1 => ( isa => NonEmptySimpleStr,
required => 1,
is => 'ro' );
- has $_ => ( isa => Str,
+ has $_ => ( isa => NonEmptySimpleStr,
required => 1,
is => 'ro',
lazy_build => 1 )
for qw/type modestr size/;
# objects can't determine their mode or filename
- has file => ( isa => Str,
+ has file => ( isa => NonEmptySimpleStr,
required => 0,
is => 'ro' );
has mode => ( isa => Int,
default => 0,
is => 'ro' );
- method BUILD {
- $self->$_() for qw/type modestr size/; # Ensure to build early.
- }
+ method BUILD { $self->$_() for qw/type size modestr/ }
- method _build_type {
- my $output = $self->run_cmd(qw/cat-file -t/, $self->{sha1});
- chomp($output);
- return $output;
+ foreach my $key (qw/ type size /) {
+ method "_build_$key" {
+ my $v = $self->_cat_file_with_flag(substr($key, 0, 1));
+ chomp($v);
+ return $v;
+ }
}
method _build_modestr {
- my $modestr = mode_to_string($self->{mode});
+ my $modestr = mode_to_string($self->mode);
return $modestr;
}
- method _build_size {
- my $output = $self->run_cmd(qw/cat-file -s/, $self->{sha1});
- chomp($output);
- return $output;
+ method _cat_file_with_flag ($flag) {
+ $self->run_cmd('cat-file', '-' . $flag, $self->{sha1})
}
=head2 contents
=cut
+ # FIXME - Should be an attribute so it gets cached?
method contents {
if ( $self->type ne 'blob' ) {
die "object $self->sha1 is not a file\n"
}
- my $output = $self->run_cmd(qw/cat-file -p/, $self->sha1);
- return unless $output;
-
- return $output;
+ $self->_cat_file_with_flag('p');
}
} # end class
class Gitalist::Git::Project {
# FIXME, use Types::Path::Class and coerce
use MooseX::Types::Common::String qw/NonEmptySimpleStr/;
- use MooseX::Types::Moose qw/Str Maybe/;
+ use MooseX::Types::Moose qw/Str Maybe Bool HashRef/;
use DateTime;
- use Path::Class;
+ use MooseX::Types::Path::Class qw/Dir/;
use Gitalist::Git::Util;
use aliased 'Gitalist::Git::Object';
has name => ( isa => NonEmptySimpleStr,
is => 'ro', required => 1 );
- has path => ( isa => "Path::Class::Dir",
+ has path => ( isa => Dir,
is => 'ro', required => 1);
has description => ( isa => Str,
has _util => ( isa => 'Gitalist::Git::Util',
is => 'ro',
lazy_build => 1,
- handles => [ 'run_cmd' ],
+ handles => [ 'run_cmd', 'get_gpp_object' ],
);
+ has project_dir => ( isa => Dir,
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ $self->is_bare
+ ? $self->path
+ : $self->path->subdir('.git')
+ },
+ );
+ has is_bare => (
+ isa => Bool,
+ is => 'ro',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ -f $self->path->file('.git', 'HEAD')
+ ? 0
+ : -f $self->path->file('HEAD')
+ ? 1
+ : confess("Cannot find " . $self->path . "/.git/HEAD or "
+ . $self->path . "/HEAD");
+ },
+ );
+
method BUILD {
$self->$_() for qw/_util last_change owner description/; # Ensure to build early.
}
method _build__util {
Gitalist::Git::Util->new(
- gitdir => $self->_project_dir($self->path),
+ project => $self,
);
}
method _build_description {
my $description = "";
eval {
- $description = $self->path->file('description')->slurp;
+ $description = $self->project_dir->file('description')->slurp;
chomp $description;
};
return $description;
}
method _build_owner {
- my ($gecos, $name) = (getpwuid $self->path->stat->uid)[6,0];
+ my ($gecos, $name) = (getpwuid $self->project_dir->stat->uid)[6,0];
$gecos =~ s/,+$//;
return length($gecos) ? $gecos : $name;
}
return @ret;
}
+ method references {
+ return $self->{references}
+ if $self->{references};
+
+ # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
+ # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
+ my $cmdout = $self->run_cmd(qw(show-ref --dereference))
+ or return;
+ my @reflist = $cmdout ? split(/\n/, $cmdout) : ();
+ my %refs;
+ for(@reflist) {
+ push @{$refs{$1}}, $2
+ if m!^($SHA1RE)\srefs/(.*)$!;
+ }
+
+ return $self->{references} = \%refs;
+}
+
+ method valid_rev (Str $rev) {
+ return ($rev =~ /^($SHA1RE)$/);
+ }
+
=head2 head_hash
: $3;
}
+ method list_revs ( NonEmptySimpleStr :$sha1!,
+ Int :$count?,
+ Int :$skip?,
+ HashRef :$search?,
+ NonEmptySimpleStr :$file?
+ ) {
+ $sha1 = $self->head_hash($sha1)
+ if !$sha1 || $sha1 !~ $SHA1RE;
+
+ my @search_opts;
+ if($search) {
+ $search->{type} = 'grep'
+ if $search->{type} eq 'commit';
+ @search_opts = (
+ # This seems a little fragile ...
+ qq[--$search->{type}=$search->{text}],
+ '--regexp-ignore-case',
+ $search->{regexp} ? '--extended-regexp' : '--fixed-strings'
+ );
+ }
+
+ my $output = $self->run_cmd(
+ 'rev-list',
+ '--header',
+ (defined $count ? "--max-count=$count" : ()),
+ (defined $skip ? "--skip=$skip" : ()),
+ @search_opts,
+ $sha1,
+ '--',
+ ($file ? $file : ()),
+ );
+ return unless $output;
+
+ my @revs = $self->parse_rev_list($output);
+
+ return @revs;
+ }
+
+ method parse_rev_list ($output) {
+ return
+ map $self->get_gpp_object($_),
+ grep $self->valid_rev($_),
+ map split(/\n/, $_, 6), split /\0/, $output;
+ }
# Compatibility
class Gitalist::Git::Repo {
use MooseX::Types::Common::String qw/NonEmptySimpleStr/;
- use Path::Class;
+ use MooseX::Types::Path::Class qw/Dir/;
+ use MooseX::Types::Moose qw/ArrayRef/;
use Gitalist::Git::Project;
- has repo_dir => ( isa => NonEmptySimpleStr,
- is => 'ro',
- required => 1 );
+ has repo_dir => (
+ isa => Dir,
+ is => 'ro',
+ required => 1,
+ coerce => 1,
+ );
method project (NonEmptySimpleStr $project) {
- my $pd = $self->dir_from_project_name($project);
return Gitalist::Git::Project->new(
name => $project,
- path => $pd,
+ path => $self->repo_dir->subdir($project),
);
}
return -f $dir->file('HEAD') || -f $dir->file('.git', 'HEAD');
}
-=head2 project_dir
-
-The directory under which the given project will reside i.e C<.git/..>
-
-=cut
-
- method project_dir ($project) {
- -f $project->file('.git', 'HEAD')
- ? $project->subdir('.git')
- : $project;
- }
-
=head2 list_projects
For the C<repo_dir> specified in the config return an array of projects where
=cut
- method list_projects {
- my $base = dir($self->repo_dir);
- my @ret;
+ has projects => (
+ isa => ArrayRef['Gitalist::Git::Project'],
+ reader => 'list_projects',
+ lazy_build => 1,
+ );
+
+ method _build_projects {
+ my $base = $self->repo_dir;
my $dh = $base->open || die "Could not open $base";
+ my @ret;
while (my $file = $dh->read) {
next if $file =~ /^.{1,2}$/;
next unless -d $obj;
next unless $self->_is_git_repo($obj);
- # FIXME - Is resolving project_dir here sane?
- # I think not, just pass $obj down, and
- # resolve $project->path and $project->is_bare
- # in BUILDARGS
- push @ret, Gitalist::Git::Project->new( name => $file,
- path => $self->project_dir($obj),
- );
+ push @ret, Gitalist::Git::Project->new(
+ name => $file,
+ path => $obj,
+ );
}
- return [sort { $a->{name} cmp $b->{name} } @ret];
+ return [sort { $a->name cmp $b->name } @ret];
}
-
-=head2 dir_from_project_name
-
-Get the corresponding directory of a given project.
-
-=cut
-
- method dir_from_project_name (Str $project) {
- return dir($self->repo_dir)->subdir($project);
- }
-
-
-
} # end class
use File::Which;
use Git::PurePerl;
use MooseX::Types::Common::String qw/NonEmptySimpleStr/;
- has gitdir => ( isa => 'Path::Class::Dir', is => 'ro', required => 1 );
+ has project => (
+ isa => 'Gitalist::Git::Project',
+ handles => { gitdir => 'project_dir' },
+ is => 'bare', # No accessor
+ weak_ref => 1, # Weak, you have to hold onto me.
+ );
has _git => ( isa => NonEmptySimpleStr, is => 'ro', lazy_build => 1 );
sub _build__git {
my $git = File::Which::which('git');
return $git;
}
- has _gpp => ( isa => 'Git::PurePerl', is => 'rw', lazy_build => 1 );
- method _build__gpp {
- my $gpp = Git::PurePerl->new(gitdir => $self->gitdir);
- return $gpp;
- }
+ has _gpp => (
+ isa => 'Git::PurePerl', is => 'ro', lazy => 1,
+ default => sub { Git::PurePerl->new(gitdir => shift->gitdir) },
+ );
method run_cmd (@args) {
unshift @args, ( '--git-dir' => $self->gitdir );
- print STDERR 'RUNNING: ', $self->_git, qq[ @args], $/;
-
+# print STDERR 'RUNNING: ', $self->_git, qq[ @args], $/;
+
open my $fh, '-|', $self->_git, @args
or die "failed to run git command";
binmode $fh, ':encoding(UTF-8)';
return $output;
}
+ method get_gpp_object (NonEmptySimpleStr $sha1) {
+ return $self->_gpp->get_object($sha1) || undef;
+ }
-
-
-
-#
} # end class
sub run_cmd {
my ($self, @args) = @_;
- print STDERR 'RUNNING: ', $self->git, qq[ @args], $/;
+# print STDERR 'RUNNING: ', $self->git, qq[ @args], $/;
open my $fh, '-|', $self->git, @args
or die "failed to run git command";
#!/usr/bin/env perl
use strict;
use warnings;
-use Test::More qw/no_plan/;
+use Test::More;
+use FindBin qw/$Bin/;
-BEGIN { use_ok 'Catalyst::Test', 'Gitalist' }
-
-# Full tests are only run if the APP_TEST env var is set.
-# This is needed to load the test configuration.
-diag("*** SKIPPING app tests.
-*** Set APP_TEST for the tests to run fully") if !$ENV{APP_TEST};
-SKIP: {
- skip "Set APP_TEST for the tests to run fully",
- 1 if !$ENV{APP_TEST};
+BEGIN {
+ $ENV{GITALIST_CONFIG} = $Bin;
+ use_ok 'Catalyst::Test', 'Gitalist'
+}
- ok( request('/')->is_success, 'Request should succeed' );
+ok( request('/')->is_success, 'Request should succeed' );
+for my $p (qw/ repo1 nodescription /) {
+ my $path = '/summary?p=' . $p;
+ ok( request($path)->is_success, "$path should succeed");
+}
+is request('/summary?p=DoesNotExist')->code, 404,
+ '/summary?p=DoesNotExist 404s';
+{
# URI tests for repo1
- use Data::Dumper;
- my $test_repo1 = curry_test_uri('repo1');
- &$test_repo1('/summary');
- &$test_repo1('/heads');
- &$test_repo1('/shortlog');
- &$test_repo1('/log');
- &$test_repo1('/commit');
- &$test_repo1('/commitdiff');
- &$test_repo1('/tree');
+ local *test = curry_test_uri('repo1');
+ test('/summary');
+ test('/shortlog');
+ test('/log');
+ test('/commit');
+ test('/commitdiff', 'h=36c6c6708b8360d7023e8a1649c45bcf9b3bd818');
+ test('/tree', 'h=145dc3ef5d307be84cb9b325d70bd08aeed0eceb;hb=36c6c6708b8360d7023e8a1649c45bcf9b3bd818');
# legacy URIs
- &$test_repo1('/', 'a=summary');
- &$test_repo1('/', 'a=heads');
- &$test_repo1('/', 'a=shortlog');
- &$test_repo1('/', 'a=log');
- &$test_repo1('/', 'a=commit');
- &$test_repo1('/', 'a=commitdiff');
- &$test_repo1('/', 'a=tree');
-# &$test_repo1('/', 'a=blob;f=file1');
-
-} # Close APP_TEST skip
+ test('/', 'a=summary');
+ test('/', 'a=heads');
+ test('/', 'a=shortlog');
+ test('/', 'a=log');
+ test('/', 'a=commit');
+ test('/', 'a=commitdiff');
+ test('/', 'a=tree');
+# $test_repo1->('/', 'a=blob;f=file1');
+}
+
+done_testing;
sub test_uri {
my ($p, $uri, $qs) = @_;
use strict;
use warnings;
use FindBin qw/$Bin/;
-use Test::More qw/no_plan/;
+use Test::More;
use Data::Dumper;
-BEGIN { use_ok 'Gitalist::Git::Util' }
+BEGIN {
+ use_ok 'Gitalist::Git::Util';
+ use_ok 'Gitalist::Git::Project';
+}
use Path::Class;
my $gitdir = dir("$Bin/lib/repositories/repo1");
-my $proj = Gitalist::Git::Util->new(
- gitdir => $gitdir,
+my $proj = Gitalist::Git::Project->new(
+ path => $gitdir,
+ name => "repo1",
);
-isa_ok($proj, 'Gitalist::Git::Util');
-
-like( $proj->_git, qr#/git$#, 'git binary found');
-isa_ok($proj->_gpp, 'Git::PurePerl', 'gpp instance created');
-is($proj->gitdir, $gitdir, 'repository path is set');
-
+my $util = Gitalist::Git::Util->new(
+ project => $proj,
+);
+isa_ok($util, 'Gitalist::Git::Util');
+like( $util->_git, qr#/git$#, 'git binary found');
+isa_ok($util->_gpp, 'Git::PurePerl', 'gpp instance created');
+done_testing;
--- /dev/null
+<Model::Git>
+ repo_dir __path_to(t/lib/repositories)__
+</Model::Git>
+
--- /dev/null
+ref: refs/heads/master
--- /dev/null
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
--- /dev/null
+#!/bin/sh
+#
+# An example hook script to check the commit log message taken by
+# applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit. The hook is
+# allowed to edit the commit message file.
+#
+# To enable this hook, make this file executable.
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/commit-msg" &&
+ exec "$GIT_DIR/hooks/commit-msg" ${1+"$@"}
+:
--- /dev/null
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by git-commit with one argument, the name of the file
+# that has the commit message. The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit. The hook is allowed to edit the commit message file.
+#
+# To enable this hook, make this file executable.
+
+# Uncomment the below to add a Signed-off-by line to the message.
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+ sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
+ echo >&2 Duplicate Signed-off-by lines.
+ exit 1
+}
+
--- /dev/null
+#!/bin/sh
+#
+# An example hook script that is called after a successful
+# commit is made.
+#
+# To enable this hook, make this file executable.
+
+: Nothing
--- /dev/null
+#!/bin/sh
+#
+# An example hook script to prepare a packed repository for use over
+# dumb transports.
+#
+# To enable this hook, make this file executable by "chmod +x post-update".
+
+exec git-update-server-info
--- /dev/null
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed
+# by applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.
+#
+# To enable this hook, make this file executable.
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/pre-commit" &&
+ exec "$GIT_DIR/hooks/pre-commit" ${1+"$@"}
+:
+
--- /dev/null
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by git-commit with no arguments. The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, make this file executable.
+
+# This is slightly modified from Andrew Morton's Perfect Patch.
+# Lines you introduce should not have trailing whitespace.
+# Also check for an indentation that has SP before a TAB.
+
+if git-rev-parse --verify HEAD 2>/dev/null
+then
+ git-diff-index -p -M --cached HEAD
+else
+ # NEEDSWORK: we should produce a diff with an empty tree here
+ # if we want to do the same verification for the initial import.
+ :
+fi |
+perl -e '
+ my $found_bad = 0;
+ my $filename;
+ my $reported_filename = "";
+ my $lineno;
+ sub bad_line {
+ my ($why, $line) = @_;
+ if (!$found_bad) {
+ print STDERR "*\n";
+ print STDERR "* You have some suspicious patch lines:\n";
+ print STDERR "*\n";
+ $found_bad = 1;
+ }
+ if ($reported_filename ne $filename) {
+ print STDERR "* In $filename\n";
+ $reported_filename = $filename;
+ }
+ print STDERR "* $why (line $lineno)\n";
+ print STDERR "$filename:$lineno:$line\n";
+ }
+ while (<>) {
+ if (m|^diff --git a/(.*) b/\1$|) {
+ $filename = $1;
+ next;
+ }
+ if (/^@@ -\S+ \+(\d+)/) {
+ $lineno = $1 - 1;
+ next;
+ }
+ if (/^ /) {
+ $lineno++;
+ next;
+ }
+ if (s/^\+//) {
+ $lineno++;
+ chomp;
+ if (/\s$/) {
+ bad_line("trailing whitespace", $_);
+ }
+ if (/^\s* /) {
+ bad_line("indent SP followed by a TAB", $_);
+ }
+ if (/^(?:[<>=]){7}/) {
+ bad_line("unresolved merge conflict", $_);
+ }
+ }
+ }
+ exit($found_bad);
+'
+
--- /dev/null
+#!/bin/sh
+#
+# Copyright (c) 2006 Junio C Hamano
+#
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+ topic="refs/heads/$2"
+else
+ topic=`git symbolic-ref HEAD`
+fi
+
+case "$basebranch,$topic" in
+master,refs/heads/??/*)
+ ;;
+*)
+ exit 0 ;# we do not interrupt others.
+ ;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master. Is it OK to rebase it?
+
+# Is topic fully merged to master?
+not_in_master=`git-rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+ echo >&2 "$topic is fully merged to master; better remove it."
+ exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next? If so you should not be rebasing it.
+only_next_1=`git-rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git-rev-list ^master ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+ not_in_topic=`git-rev-list "^$topic" master`
+ if test -z "$not_in_topic"
+ then
+ echo >&2 "$topic is already up-to-date with master"
+ exit 1 ;# we could allow it, but there is no point.
+ else
+ exit 0
+ fi
+else
+ not_in_next=`git-rev-list --pretty=oneline ^${publish} "$topic"`
+ perl -e '
+ my $topic = $ARGV[0];
+ my $msg = "* $topic has commits already merged to public branch:\n";
+ my (%not_in_next) = map {
+ /^([0-9a-f]+) /;
+ ($1 => 1);
+ } split(/\n/, $ARGV[1]);
+ for my $elem (map {
+ /^([0-9a-f]+) (.*)$/;
+ [$1 => $2];
+ } split(/\n/, $ARGV[2])) {
+ if (!exists $not_in_next{$elem->[0]}) {
+ if ($msg) {
+ print STDERR $msg;
+ undef $msg;
+ }
+ print STDERR " $elem->[1]\n";
+ }
+ }
+ ' "$topic" "$not_in_next" "$not_in_master"
+ exit 1
+fi
+
+exit 0
+
+################################################################
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+ merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+ it is deleted. If you need to build on top of it to correct
+ earlier mistakes, a new topic branch is created by forking at
+ the tip of the "master". This is not strictly necessary, but
+ it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+ branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next". Young
+ topic branches can have stupid mistakes you would rather
+ clean up before publishing, and things that have not been
+ merged into other branches can be easily rebased without
+ affecting other people. But once it is published, you would
+ not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+ Then you can delete it. More importantly, you should not
+ build on top of it -- other people may already want to
+ change things related to the topic as patches against your
+ "master", so if you need further changes, it is better to
+ fork the topic (perhaps with the same name) afresh from the
+ tip of "master".
+
+Let's look at this example:
+
+ o---o---o---o---o---o---o---o---o---o "next"
+ / / / /
+ / a---a---b A / /
+ / / / /
+ / / c---c---c---c B /
+ / / / \ /
+ / / / b---b C \ /
+ / / / / \ /
+ ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished. It has been fully merged up to "master" and "next",
+ and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+ git-rev-list ^master ^topic next
+ git-rev-list ^master next
+
+ if these match, topic has not merged in next at all.
+
+To compute (2):
+
+ git-rev-list master..topic
+
+ if this is empty, it is fully merged to "master".
--- /dev/null
+#!/bin/sh
+#
+# An example hook script to mail out commit update information.
+# It can also blocks tags that aren't annotated.
+# Called by git-receive-pack with arguments: refname sha1-old sha1-new
+#
+# To enable this hook, make this file executable by "chmod +x update".
+#
+# Config
+# ------
+# hooks.mailinglist
+# This is the list that all pushes will go to; leave it blank to not send
+# emails frequently. The log email will list every log entry in full between
+# the old ref value and the new ref value.
+# hooks.announcelist
+# This is the list that all pushes of annotated tags will go to. Leave it
+# blank to just use the mailinglist field. The announce emails list the
+# short log summary of the changes since the last annotated tag
+# hooks.allowunannotated
+# This boolean sets whether unannotated tags will be allowed into the
+# repository. By default they won't be.
+#
+# Notes
+# -----
+# All emails have their subjects prefixed with "[SCM]" to aid filtering.
+# All emails include the headers "X-Git-Refname", "X-Git-Oldrev",
+# "X-Git-Newrev", and "X-Git-Reftype" to enable fine tuned filtering and info.
+
+# --- Constants
+EMAILPREFIX="[SCM] "
+LOGBEGIN="- Log -----------------------------------------------------------------"
+LOGEND="-----------------------------------------------------------------------"
+DATEFORMAT="%F %R %z"
+
+# --- Command line
+refname="$1"
+oldrev="$2"
+newrev="$3"
+
+# --- Safety check
+if [ -z "$GIT_DIR" ]; then
+ echo "Don't run this script from the command line." >&2
+ echo " (if you want, you could supply GIT_DIR then run" >&2
+ echo " $0 <ref> <oldrev> <newrev>)" >&2
+ exit 1
+fi
+
+if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
+ echo "Usage: $0 <ref> <oldrev> <newrev>" >&2
+ exit 1
+fi
+
+# --- Config
+projectdesc=$(cat $GIT_DIR/description)
+recipients=$(git-repo-config hooks.mailinglist)
+announcerecipients=$(git-repo-config hooks.announcelist)
+allowunannotated=$(git-repo-config --bool hooks.allowunannotated)
+
+# --- Check types
+newrev_type=$(git-cat-file -t "$newrev")
+
+case "$refname","$newrev_type" in
+ refs/tags/*,commit)
+ # un-annotated tag
+ refname_type="tag"
+ short_refname=${refname##refs/tags/}
+ if [ $allowunannotated != "true" ]; then
+ echo "*** The un-annotated tag, $short_refname is not allowed in this repository" >&2
+ echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
+ exit 1
+ fi
+ ;;
+ refs/tags/*,tag)
+ # annotated tag
+ refname_type="annotated tag"
+ short_refname=${refname##refs/tags/}
+ # change recipients
+ if [ -n "$announcerecipients" ]; then
+ recipients="$announcerecipients"
+ fi
+ ;;
+ refs/heads/*,commit)
+ # branch
+ refname_type="branch"
+ short_refname=${refname##refs/heads/}
+ ;;
+ refs/remotes/*,commit)
+ # tracking branch
+ refname_type="tracking branch"
+ short_refname=${refname##refs/remotes/}
+ # Should this even be allowed?
+ echo "*** Push-update of tracking branch, $refname. No email generated." >&2
+ exit 0
+ ;;
+ *)
+ # Anything else (is there anything else?)
+ echo "*** Update hook: unknown type of update, \"$newrev_type\", to ref $refname" >&2
+ exit 1
+ ;;
+esac
+
+# Check if we've got anyone to send to
+if [ -z "$recipients" ]; then
+ # If the email isn't sent, then at least give the user some idea of what command
+ # would generate the email at a later date
+ echo "*** No recipients found - no email will be sent, but the push will continue" >&2
+ echo "*** for $0 $1 $2 $3" >&2
+ exit 0
+fi
+
+# --- Email parameters
+committer=$(git show --pretty=full -s $newrev | grep "^Commit: " | sed -e "s/^Commit: //")
+describe=$(git describe $newrev 2>/dev/null)
+if [ -z "$describe" ]; then
+ describe=$newrev
+fi
+
+# --- Email (all stdout will be the email)
+(
+# Generate header
+cat <<-EOF
+From: $committer
+To: $recipients
+Subject: ${EMAILPREFIX}$projectdesc $refname_type, $short_refname now at $describe
+X-Git-Refname: $refname
+X-Git-Reftype: $refname_type
+X-Git-Oldrev: $oldrev
+X-Git-Newrev: $newrev
+
+Hello,
+
+This is an automated email from the git hooks/update script, it was
+generated because a ref change was pushed to the repository.
+
+Updating $refname_type, $short_refname,
+EOF
+
+case "$refname_type" in
+ "tracking branch"|branch)
+ if expr "$oldrev" : '0*$' >/dev/null
+ then
+ # If the old reference is "0000..0000" then this is a new branch
+ # and so oldrev is not valid
+ echo " as a new $refname_type"
+ echo " to $newrev ($newrev_type)"
+ echo ""
+ echo $LOGBEGIN
+ # This shows all log entries that are not already covered by
+ # another ref - i.e. commits that are now accessible from this
+ # ref that were previously not accessible
+ git-rev-parse --not --all | git-rev-list --stdin --pretty $newref
+ echo $LOGEND
+ else
+ # oldrev is valid
+ oldrev_type=$(git-cat-file -t "$oldrev")
+
+ # Now the problem is for cases like this:
+ # * --- * --- * --- * (oldrev)
+ # \
+ # * --- * --- * (newrev)
+ # i.e. there is no guarantee that newrev is a strict subset
+ # of oldrev - (would have required a force, but that's allowed).
+ # So, we can't simply say rev-list $oldrev..$newrev. Instead
+ # we find the common base of the two revs and list from there
+ baserev=$(git-merge-base $oldrev $newrev)
+
+ # Commit with a parent
+ for rev in $(git-rev-list $newrev ^$baserev)
+ do
+ revtype=$(git-cat-file -t "$rev")
+ echo " via $rev ($revtype)"
+ done
+ if [ "$baserev" = "$oldrev" ]; then
+ echo " from $oldrev ($oldrev_type)"
+ else
+ echo " based on $baserev"
+ echo " from $oldrev ($oldrev_type)"
+ echo ""
+ echo "This ref update crossed a branch point; i.e. the old rev is not a strict subset"
+ echo "of the new rev. This occurs, when you --force push a change in a situation"
+ echo "like this:"
+ echo ""
+ echo " * -- * -- B -- O -- O -- O ($oldrev)"
+ echo " \\"
+ echo " N -- N -- N ($newrev)"
+ echo ""
+ echo "Therefore, we assume that you've already had alert emails for all of the O"
+ echo "revisions, and now give you all the revisions in the N branch from the common"
+ echo "base, B ($baserev), up to the new revision."
+ fi
+ echo ""
+ echo $LOGBEGIN
+ git-rev-list --pretty $newrev ^$baserev
+ echo $LOGEND
+ echo ""
+ echo "Diffstat:"
+ git-diff-tree --no-color --stat -M -C --find-copies-harder $newrev ^$baserev
+ fi
+ ;;
+ "annotated tag")
+ # Should we allow changes to annotated tags?
+ if expr "$oldrev" : '0*$' >/dev/null
+ then
+ # If the old reference is "0000..0000" then this is a new atag
+ # and so oldrev is not valid
+ echo " to $newrev ($newrev_type)"
+ else
+ echo " to $newrev ($newrev_type)"
+ echo " from $oldrev"
+ fi
+
+ # If this tag succeeds another, then show which tag it replaces
+ prevtag=$(git describe $newrev^ 2>/dev/null | sed 's/-g.*//')
+ if [ -n "$prevtag" ]; then
+ echo " replaces $prevtag"
+ fi
+
+ # Read the tag details
+ eval $(git cat-file tag $newrev | \
+ sed -n '4s/tagger \([^>]*>\)[^0-9]*\([0-9]*\).*/tagger="\1" ts="\2"/p')
+ tagged=$(date --date="1970-01-01 00:00:00 +0000 $ts seconds" +"$DATEFORMAT")
+
+ echo " tagged by $tagger"
+ echo " on $tagged"
+
+ echo ""
+ echo $LOGBEGIN
+ echo ""
+
+ if [ -n "$prevtag" ]; then
+ git rev-list --pretty=short "$prevtag..$newrev" | git shortlog
+ else
+ git rev-list --pretty=short $newrev | git shortlog
+ fi
+
+ echo $LOGEND
+ echo ""
+ ;;
+ *)
+ # By default, unannotated tags aren't allowed in; if
+ # they are though, it's debatable whether we would even want an
+ # email to be generated; however, I don't want to add another config
+ # option just for that.
+ #
+ # Unannotated tags are more about marking a point than releasing
+ # a version; therefore we don't do the shortlog summary that we
+ # do for annotated tags above - we simply show that the point has
+ # been marked, and print the log message for the marked point for
+ # reference purposes
+ #
+ # Note this section also catches any other reference type (although
+ # there aren't any) and deals with them in the same way.
+ if expr "$oldrev" : '0*$' >/dev/null
+ then
+ # If the old reference is "0000..0000" then this is a new tag
+ # and so oldrev is not valid
+ echo " as a new $refname_type"
+ echo " to $newrev ($newrev_type)"
+ else
+ echo " to $newrev ($newrev_type)"
+ echo " from $oldrev"
+ fi
+ echo ""
+ echo $LOGBEGIN
+ git-show --no-color --root -s $newrev
+ echo $LOGEND
+ echo ""
+ ;;
+esac
+
+# Footer
+cat <<-EOF
+
+hooks/update
+---
+Git Source Code Management System
+$0 $1 \\
+ $2 \\
+ $3
+EOF
+#) | cat >&2
+) | /usr/sbin/sendmail -t
+
+# --- Finished
+exit 0
--- /dev/null
+# git-ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
--- /dev/null
+xÎAj\ 31\f@Ñ®}
+í\vdGc(¥«\1e 7°,95L2Ÿ\14zú\1e!Û\ f\ f~=n·>!Æü4;Ь²·d¢¾-U³rL&¨[q7ôê\1a>Ëðû\ 4nV©¢ÖÄŵ-9®KfÍÑ8oÌL´z(_óã\18ð¾\1f£;\Êõ»Ï\1fx\19¥íoæú\17OǸ¾\ 2p$à\19 1ÔÿÅé\ fáPÌÀú së»Çð\vk:H
\ No newline at end of file
--- /dev/null
+36c6c6708b8360d7023e8a1649c45bcf9b3bd818
ok( ! $m->is_git_repo( $repoEmpty ), 'is_git_repo is false for empty dir' );
my $projectList = $m->list_projects('t/lib/repositories');
-ok( scalar @{$projectList} == 2, 'list_projects returns an array with the correct number of members' );
+ok( scalar @{$projectList} == 3, 'list_projects returns an array with the correct number of members' );
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' );