mirror of
https://github.com/git/git.git
synced 2024-11-06 09:13:01 +01:00
Merge branch 'jc/pack-reuse'
* jc/pack-reuse: git-repack: allow passing a couple of flags to pack-objects. pack-objects: finishing touches. pack-objects: reuse data from existing packs. Add contrib/gitview from Aneesh. git-svn: ensure fetch always works chronologically. git-svn: fix revision order when XML::Simple is not loaded Introducing contrib/git-svn. Allow building Git in systems without iconv
This commit is contained in:
commit
07e8ab9be9
11 changed files with 2392 additions and 69 deletions
|
@ -8,7 +8,10 @@ git-pack-objects - Create a packed archive of objects.
|
|||
|
||||
SYNOPSIS
|
||||
--------
|
||||
'git-pack-objects' [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list
|
||||
[verse]
|
||||
'git-pack-objects' [-q] [--no-reuse-delta] [--non-empty]
|
||||
[--local] [--incremental] [--window=N] [--depth=N]
|
||||
{--stdout | base-name} < object-list
|
||||
|
||||
|
||||
DESCRIPTION
|
||||
|
@ -32,6 +35,10 @@ Placing both in the pack/ subdirectory of $GIT_OBJECT_DIRECTORY (or
|
|||
any of the directories on $GIT_ALTERNATE_OBJECT_DIRECTORIES)
|
||||
enables git to read from such an archive.
|
||||
|
||||
In a packed archive, an object is either stored as a compressed
|
||||
whole, or as a difference from some other object. The latter is
|
||||
often called a delta.
|
||||
|
||||
|
||||
OPTIONS
|
||||
-------
|
||||
|
@ -74,6 +81,18 @@ base-name::
|
|||
Only create a packed archive if it would contain at
|
||||
least one object.
|
||||
|
||||
-q::
|
||||
This flag makes the command not to report its progress
|
||||
on the standard error stream.
|
||||
|
||||
--no-reuse-delta::
|
||||
When creating a packed archive in a repository that
|
||||
has existing packs, the command reuses existing deltas.
|
||||
This sometimes results in a slightly suboptimal pack.
|
||||
This flag tells the command not to reuse existing deltas
|
||||
but compute them from scratch.
|
||||
|
||||
|
||||
Author
|
||||
------
|
||||
Written by Linus Torvalds <torvalds@osdl.org>
|
||||
|
|
|
@ -9,7 +9,7 @@ objects into pack files.
|
|||
|
||||
SYNOPSIS
|
||||
--------
|
||||
'git-repack' [-a] [-d] [-l] [-n]
|
||||
'git-repack' [-a] [-d] [-f] [-l] [-n] [-q]
|
||||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
|
@ -43,6 +43,14 @@ OPTIONS
|
|||
Pass the `--local` option to `git pack-objects`, see
|
||||
gitlink:git-pack-objects[1].
|
||||
|
||||
-f::
|
||||
Pass the `--no-reuse-delta` option to `git pack-objects`, see
|
||||
gitlink:git-pack-objects[1].
|
||||
|
||||
-q::
|
||||
Pass the `-q` option to `git pack-objects`, see
|
||||
gitlink:git-pack-objects[1].
|
||||
|
||||
-n::
|
||||
Do not update the server information with
|
||||
`git update-server-info`.
|
||||
|
|
6
Makefile
6
Makefile
|
@ -53,6 +53,8 @@ all:
|
|||
# Define NO_SOCKADDR_STORAGE if your platform does not have struct
|
||||
# sockaddr_storage.
|
||||
#
|
||||
# Define NO_ICONV if your libc does not properly support iconv.
|
||||
#
|
||||
# Define COLLISION_CHECK below if you believe that SHA1's
|
||||
# 1461501637330902918203684832716283019655932542976 hashes do not give you
|
||||
# sufficient guarantee that no collisions between objects will ever happen.
|
||||
|
@ -380,6 +382,10 @@ else
|
|||
endif
|
||||
endif
|
||||
|
||||
ifdef NO_ICONV
|
||||
ALL_CFLAGS += -DNO_ICONV
|
||||
endif
|
||||
|
||||
ifdef PPC_SHA1
|
||||
SHA1_HEADER = "ppc/sha1.h"
|
||||
LIB_OBJS += ppc/sha1.o ppc/sha1ppc.o
|
||||
|
|
771
contrib/git-svn/git-svn
Executable file
771
contrib/git-svn/git-svn
Executable file
|
@ -0,0 +1,771 @@
|
|||
#!/usr/bin/env perl
|
||||
use warnings;
|
||||
use strict;
|
||||
use vars qw/ $AUTHOR $VERSION
|
||||
$SVN_URL $SVN_INFO $SVN_WC
|
||||
$GIT_SVN_INDEX $GIT_SVN
|
||||
$GIT_DIR $REV_DIR/;
|
||||
$AUTHOR = 'Eric Wong <normalperson@yhbt.net>';
|
||||
$VERSION = '0.9.0';
|
||||
$GIT_DIR = $ENV{GIT_DIR} || "$ENV{PWD}/.git";
|
||||
$GIT_SVN = $ENV{GIT_SVN_ID} || 'git-svn';
|
||||
$GIT_SVN_INDEX = "$GIT_DIR/$GIT_SVN/index";
|
||||
$ENV{GIT_DIR} ||= $GIT_DIR;
|
||||
$SVN_URL = undef;
|
||||
$REV_DIR = "$GIT_DIR/$GIT_SVN/revs";
|
||||
$SVN_WC = "$GIT_DIR/$GIT_SVN/tree";
|
||||
|
||||
# make sure the svn binary gives consistent output between locales and TZs:
|
||||
$ENV{TZ} = 'UTC';
|
||||
$ENV{LC_ALL} = 'C';
|
||||
|
||||
# If SVN:: library support is added, please make the dependencies
|
||||
# optional and preserve the capability to use the command-line client.
|
||||
# See what I do with XML::Simple to make the dependency optional.
|
||||
use Carp qw/croak/;
|
||||
use IO::File qw//;
|
||||
use File::Basename qw/dirname basename/;
|
||||
use File::Path qw/mkpath/;
|
||||
use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/;
|
||||
use File::Spec qw//;
|
||||
my $sha1 = qr/[a-f\d]{40}/;
|
||||
my $sha1_short = qr/[a-f\d]{6,40}/;
|
||||
my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit);
|
||||
|
||||
GetOptions( 'revision|r=s' => \$_revision,
|
||||
'no-ignore-externals' => \$_no_ignore_ext,
|
||||
'stdin|' => \$_stdin,
|
||||
'edit|e' => \$_edit,
|
||||
'rmdir' => \$_rmdir,
|
||||
'help|H|h' => \$_help,
|
||||
'no-stop-copy' => \$_no_stop_copy );
|
||||
my %cmd = (
|
||||
fetch => [ \&fetch, "Download new revisions from SVN" ],
|
||||
init => [ \&init, "Initialize and fetch (import)"],
|
||||
commit => [ \&commit, "Commit git revisions to SVN" ],
|
||||
rebuild => [ \&rebuild, "Rebuild git-svn metadata (after git clone)" ],
|
||||
help => [ \&usage, "Show help" ],
|
||||
);
|
||||
my $cmd;
|
||||
for (my $i = 0; $i < @ARGV; $i++) {
|
||||
if (defined $cmd{$ARGV[$i]}) {
|
||||
$cmd = $ARGV[$i];
|
||||
splice @ARGV, $i, 1;
|
||||
last;
|
||||
}
|
||||
};
|
||||
|
||||
# we may be called as git-svn-(command), or git-svn(command).
|
||||
foreach (keys %cmd) {
|
||||
if (/git\-svn\-?($_)(?:\.\w+)?$/) {
|
||||
$cmd = $1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
usage(0) if $_help;
|
||||
usage(1) unless (defined $cmd);
|
||||
svn_check_ignore_externals();
|
||||
$cmd{$cmd}->[0]->(@ARGV);
|
||||
exit 0;
|
||||
|
||||
####################### primary functions ######################
|
||||
sub usage {
|
||||
my $exit = shift || 0;
|
||||
my $fd = $exit ? \*STDERR : \*STDOUT;
|
||||
print $fd <<"";
|
||||
git-svn - bidirectional operations between a single Subversion tree and git
|
||||
Usage: $0 <command> [options] [arguments]\n
|
||||
Available commands:
|
||||
|
||||
foreach (sort keys %cmd) {
|
||||
print $fd ' ',pack('A10',$_),$cmd{$_}->[1],"\n";
|
||||
}
|
||||
print $fd <<"";
|
||||
\nGIT_SVN_ID may be set in the environment to an arbitrary identifier if
|
||||
you're tracking multiple SVN branches/repositories in one git repository
|
||||
and want to keep them separate.
|
||||
|
||||
exit $exit;
|
||||
}
|
||||
|
||||
sub rebuild {
|
||||
$SVN_URL = shift or undef;
|
||||
my $repo_uuid;
|
||||
my $newest_rev = 0;
|
||||
|
||||
my $pid = open(my $rev_list,'-|');
|
||||
defined $pid or croak $!;
|
||||
if ($pid == 0) {
|
||||
exec("git-rev-list","$GIT_SVN-HEAD") or croak $!;
|
||||
}
|
||||
my $first;
|
||||
while (<$rev_list>) {
|
||||
chomp;
|
||||
my $c = $_;
|
||||
croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o;
|
||||
my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`);
|
||||
next if (!@commit); # skip merges
|
||||
my $id = $commit[$#commit];
|
||||
my ($url, $rev, $uuid) = ($id =~ /^git-svn-id:\s(\S+?)\@(\d+)
|
||||
\s([a-f\d\-]+)$/x);
|
||||
if (!$rev || !$uuid || !$url) {
|
||||
# some of the original repositories I made had
|
||||
# indentifiers like this:
|
||||
($rev, $uuid) = ($id =~/^git-svn-id:\s(\d+)
|
||||
\@([a-f\d\-]+)/x);
|
||||
if (!$rev || !$uuid) {
|
||||
croak "Unable to extract revision or UUID from ",
|
||||
"$c, $id\n";
|
||||
}
|
||||
}
|
||||
print "r$rev = $c\n";
|
||||
unless (defined $first) {
|
||||
if (!$SVN_URL && !$url) {
|
||||
croak "SVN repository location required: $url\n";
|
||||
}
|
||||
$SVN_URL ||= $url;
|
||||
$repo_uuid = setup_git_svn();
|
||||
$first = $rev;
|
||||
}
|
||||
if ($uuid ne $repo_uuid) {
|
||||
croak "Repository UUIDs do not match!\ngot: $uuid\n",
|
||||
"expected: $repo_uuid\n";
|
||||
}
|
||||
assert_revision_eq_or_unknown($rev, $c);
|
||||
sys('git-update-ref',"$GIT_SVN/revs/$rev",$c);
|
||||
$newest_rev = $rev if ($rev > $newest_rev);
|
||||
}
|
||||
close $rev_list or croak $?;
|
||||
if (!chdir $SVN_WC) {
|
||||
my @svn_co = ('svn','co',"-r$first");
|
||||
push @svn_co, '--ignore-externals' unless $_no_ignore_ext;
|
||||
sys(@svn_co, $SVN_URL, $SVN_WC);
|
||||
chdir $SVN_WC or croak $!;
|
||||
}
|
||||
|
||||
$pid = fork;
|
||||
defined $pid or croak $!;
|
||||
if ($pid == 0) {
|
||||
my @svn_up = qw(svn up);
|
||||
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
|
||||
sys(@svn_up,"-r$newest_rev");
|
||||
$ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
|
||||
git_addremove();
|
||||
exec('git-write-tree');
|
||||
}
|
||||
waitpid $pid, 0;
|
||||
}
|
||||
|
||||
sub init {
|
||||
$SVN_URL = shift or croak "SVN repository location required\n";
|
||||
unless (-d $GIT_DIR) {
|
||||
sys('git-init-db');
|
||||
}
|
||||
setup_git_svn();
|
||||
}
|
||||
|
||||
sub fetch {
|
||||
my (@parents) = @_;
|
||||
$SVN_URL ||= file_to_s("$GIT_DIR/$GIT_SVN/info/url");
|
||||
my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL);
|
||||
unless ($_revision) {
|
||||
$_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD';
|
||||
}
|
||||
push @log_args, "-r$_revision";
|
||||
push @log_args, '--stop-on-copy' unless $_no_stop_copy;
|
||||
|
||||
eval { require XML::Simple or croak $! };
|
||||
my $svn_log = $@ ? svn_log_raw(@log_args) : svn_log_xml(@log_args);
|
||||
@$svn_log = sort { $a->{revision} <=> $b->{revision} } @$svn_log;
|
||||
|
||||
my $base = shift @$svn_log or croak "No base revision!\n";
|
||||
my $last_commit = undef;
|
||||
unless (-d $SVN_WC) {
|
||||
my @svn_co = ('svn','co',"-r$base->{revision}");
|
||||
push @svn_co,'--ignore-externals' unless $_no_ignore_ext;
|
||||
sys(@svn_co, $SVN_URL, $SVN_WC);
|
||||
chdir $SVN_WC or croak $!;
|
||||
$last_commit = git_commit($base, @parents);
|
||||
unless (-f "$GIT_DIR/refs/heads/master") {
|
||||
sys(qw(git-update-ref refs/heads/master),$last_commit);
|
||||
}
|
||||
assert_svn_wc_clean($base->{revision}, $last_commit);
|
||||
} else {
|
||||
chdir $SVN_WC or croak $!;
|
||||
$last_commit = file_to_s("$REV_DIR/$base->{revision}");
|
||||
}
|
||||
my @svn_up = qw(svn up);
|
||||
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
|
||||
my $last_rev = $base->{revision};
|
||||
foreach my $log_msg (@$svn_log) {
|
||||
assert_svn_wc_clean($last_rev, $last_commit);
|
||||
$last_rev = $log_msg->{revision};
|
||||
sys(@svn_up,"-r$last_rev");
|
||||
$last_commit = git_commit($log_msg, $last_commit, @parents);
|
||||
}
|
||||
assert_svn_wc_clean($last_rev, $last_commit);
|
||||
return pop @$svn_log;
|
||||
}
|
||||
|
||||
sub commit {
|
||||
my (@commits) = @_;
|
||||
if ($_stdin || !@commits) {
|
||||
print "Reading from stdin...\n";
|
||||
@commits = ();
|
||||
while (<STDIN>) {
|
||||
if (/^([a-f\d]{6,40})\b/) {
|
||||
unshift @commits, $1;
|
||||
}
|
||||
}
|
||||
}
|
||||
my @revs;
|
||||
foreach (@commits) {
|
||||
push @revs, (safe_qx('git-rev-parse',$_));
|
||||
}
|
||||
chomp @revs;
|
||||
|
||||
fetch();
|
||||
chdir $SVN_WC or croak $!;
|
||||
my $svn_current_rev = svn_info('.')->{'Last Changed Rev'};
|
||||
foreach my $c (@revs) {
|
||||
print "Committing $c\n";
|
||||
svn_checkout_tree($svn_current_rev, $c);
|
||||
$svn_current_rev = svn_commit_tree($svn_current_rev, $c);
|
||||
}
|
||||
print "Done committing ",scalar @revs," revisions to SVN\n";
|
||||
|
||||
}
|
||||
|
||||
########################### utility functions #########################
|
||||
|
||||
sub setup_git_svn {
|
||||
defined $SVN_URL or croak "SVN repository location required\n";
|
||||
unless (-d $GIT_DIR) {
|
||||
croak "GIT_DIR=$GIT_DIR does not exist!\n";
|
||||
}
|
||||
mkpath(["$GIT_DIR/$GIT_SVN"]);
|
||||
mkpath(["$GIT_DIR/$GIT_SVN/info"]);
|
||||
mkpath([$REV_DIR]);
|
||||
s_to_file($SVN_URL,"$GIT_DIR/$GIT_SVN/info/url");
|
||||
my $uuid = svn_info($SVN_URL)->{'Repository UUID'} or
|
||||
croak "Repository UUID unreadable\n";
|
||||
s_to_file($uuid,"$GIT_DIR/$GIT_SVN/info/uuid");
|
||||
|
||||
open my $fd, '>>', "$GIT_DIR/$GIT_SVN/info/exclude" or croak $!;
|
||||
print $fd '.svn',"\n";
|
||||
close $fd or croak $!;
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
sub assert_svn_wc_clean {
|
||||
my ($svn_rev, $commit) = @_;
|
||||
croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/);
|
||||
croak "$commit is not a sha1!\n" unless ($commit =~ /^$sha1$/o);
|
||||
my $svn_info = svn_info('.');
|
||||
if ($svn_rev != $svn_info->{'Last Changed Rev'}) {
|
||||
croak "Expected r$svn_rev, got r",
|
||||
$svn_info->{'Last Changed Rev'},"\n";
|
||||
}
|
||||
my @status = grep(!/^Performing status on external/,(`svn status`));
|
||||
@status = grep(!/^\s*$/,@status);
|
||||
if (scalar @status) {
|
||||
print STDERR "Tree ($SVN_WC) is not clean:\n";
|
||||
print STDERR $_ foreach @status;
|
||||
croak;
|
||||
}
|
||||
my ($tree_a) = grep(/^tree $sha1$/o,`git-cat-file commit $commit`);
|
||||
$tree_a =~ s/^tree //;
|
||||
chomp $tree_a;
|
||||
chomp(my $tree_b = `GIT_INDEX_FILE=$GIT_SVN_INDEX git-write-tree`);
|
||||
if ($tree_a ne $tree_b) {
|
||||
croak "$svn_rev != $commit, $tree_a != $tree_b\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub parse_diff_tree {
|
||||
my $diff_fh = shift;
|
||||
local $/ = "\0";
|
||||
my $state = 'meta';
|
||||
my @mods;
|
||||
while (<$diff_fh>) {
|
||||
chomp $_; # this gets rid of the trailing "\0"
|
||||
print $_,"\n";
|
||||
if ($state eq 'meta' && /^:(\d{6})\s(\d{6})\s
|
||||
$sha1\s($sha1)\s([MTCRAD])\d*$/xo) {
|
||||
push @mods, { mode_a => $1, mode_b => $2,
|
||||
sha1_b => $3, chg => $4 };
|
||||
if ($4 =~ /^(?:C|R)$/) {
|
||||
$state = 'file_a';
|
||||
} else {
|
||||
$state = 'file_b';
|
||||
}
|
||||
} elsif ($state eq 'file_a') {
|
||||
my $x = $mods[$#mods] or croak __LINE__,": Empty array\n";
|
||||
if ($x->{chg} !~ /^(?:C|R)$/) {
|
||||
croak __LINE__,": Error parsing $_, $x->{chg}\n";
|
||||
}
|
||||
$x->{file_a} = $_;
|
||||
$state = 'file_b';
|
||||
} elsif ($state eq 'file_b') {
|
||||
my $x = $mods[$#mods] or croak __LINE__,": Empty array\n";
|
||||
if (exists $x->{file_a} && $x->{chg} !~ /^(?:C|R)$/) {
|
||||
croak __LINE__,": Error parsing $_, $x->{chg}\n";
|
||||
}
|
||||
if (!exists $x->{file_a} && $x->{chg} =~ /^(?:C|R)$/) {
|
||||
croak __LINE__,": Error parsing $_, $x->{chg}\n";
|
||||
}
|
||||
$x->{file_b} = $_;
|
||||
$state = 'meta';
|
||||
} else {
|
||||
croak __LINE__,": Error parsing $_\n";
|
||||
}
|
||||
}
|
||||
close $diff_fh or croak $!;
|
||||
return \@mods;
|
||||
}
|
||||
|
||||
sub svn_check_prop_executable {
|
||||
my $m = shift;
|
||||
if ($m->{mode_b} =~ /755$/ && $m->{mode_a} !~ /755$/) {
|
||||
sys(qw(svn propset svn:executable 1), $m->{file_b});
|
||||
} elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) {
|
||||
sys(qw(svn propdel svn:executable), $m->{file_b});
|
||||
}
|
||||
}
|
||||
|
||||
sub svn_ensure_parent_path {
|
||||
my $dir_b = dirname(shift);
|
||||
svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir);
|
||||
mkpath([$dir_b]) unless (-d $dir_b);
|
||||
sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn");
|
||||
}
|
||||
|
||||
sub svn_checkout_tree {
|
||||
my ($svn_rev, $commit) = @_;
|
||||
my $from = file_to_s("$REV_DIR/$svn_rev");
|
||||
assert_svn_wc_clean($svn_rev,$from);
|
||||
print "diff-tree '$from' '$commit'\n";
|
||||
my $pid = open my $diff_fh, '-|';
|
||||
defined $pid or croak $!;
|
||||
if ($pid == 0) {
|
||||
exec(qw(git-diff-tree -z -r -C), $from, $commit) or croak $!;
|
||||
}
|
||||
my $mods = parse_diff_tree($diff_fh);
|
||||
unless (@$mods) {
|
||||
# git can do empty commits, SVN doesn't allow it...
|
||||
return $svn_rev;
|
||||
}
|
||||
my %rm;
|
||||
foreach my $m (@$mods) {
|
||||
if ($m->{chg} eq 'C') {
|
||||
svn_ensure_parent_path( $m->{file_b} );
|
||||
sys(qw(svn cp), $m->{file_a}, $m->{file_b});
|
||||
blob_to_file( $m->{sha1_b}, $m->{file_b});
|
||||
svn_check_prop_executable($m);
|
||||
} elsif ($m->{chg} eq 'D') {
|
||||
$rm{dirname $m->{file_b}}->{basename $m->{file_b}} = 1;
|
||||
sys(qw(svn rm --force), $m->{file_b});
|
||||
} elsif ($m->{chg} eq 'R') {
|
||||
svn_ensure_parent_path( $m->{file_b} );
|
||||
sys(qw(svn mv --force), $m->{file_a}, $m->{file_b});
|
||||
blob_to_file( $m->{sha1_b}, $m->{file_b});
|
||||
svn_check_prop_executable($m);
|
||||
$rm{dirname $m->{file_a}}->{basename $m->{file_a}} = 1;
|
||||
} elsif ($m->{chg} eq 'M') {
|
||||
if ($m->{mode_b} =~ /^120/ && $m->{mode_a} =~ /^120/) {
|
||||
unlink $m->{file_b} or croak $!;
|
||||
blob_to_symlink($m->{sha1_b}, $m->{file_b});
|
||||
} else {
|
||||
blob_to_file($m->{sha1_b}, $m->{file_b});
|
||||
}
|
||||
svn_check_prop_executable($m);
|
||||
} elsif ($m->{chg} eq 'T') {
|
||||
sys(qw(svn rm --force),$m->{file_b});
|
||||
if ($m->{mode_b} =~ /^120/ && $m->{mode_a} =~ /^100/) {
|
||||
blob_to_symlink($m->{sha1_b}, $m->{file_b});
|
||||
} else {
|
||||
blob_to_file($m->{sha1_b}, $m->{file_b});
|
||||
}
|
||||
svn_check_prop_executable($m);
|
||||
sys(qw(svn add --force), $m->{file_b});
|
||||
} elsif ($m->{chg} eq 'A') {
|
||||
svn_ensure_parent_path( $m->{file_b} );
|
||||
blob_to_file( $m->{sha1_b}, $m->{file_b});
|
||||
if ($m->{mode_b} =~ /755$/) {
|
||||
chmod 0755, $m->{file_b};
|
||||
}
|
||||
sys(qw(svn add --force), $m->{file_b});
|
||||
} else {
|
||||
croak "Invalid chg: $m->{chg}\n";
|
||||
}
|
||||
}
|
||||
if ($_rmdir) {
|
||||
my $old_index = $ENV{GIT_INDEX_FILE};
|
||||
$ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
|
||||
foreach my $dir (keys %rm) {
|
||||
my $files = $rm{$dir};
|
||||
my @files;
|
||||
foreach (safe_qx('svn','ls',$dir)) {
|
||||
chomp;
|
||||
push @files, $_ unless $files->{$_};
|
||||
}
|
||||
sys(qw(svn rm),$dir) unless @files;
|
||||
}
|
||||
if ($old_index) {
|
||||
$ENV{GIT_INDEX_FILE} = $old_index;
|
||||
} else {
|
||||
delete $ENV{GIT_INDEX_FILE};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub svn_commit_tree {
|
||||
my ($svn_rev, $commit) = @_;
|
||||
my $commit_msg = "$GIT_DIR/$GIT_SVN/.svn-commit.tmp.$$";
|
||||
open my $msg, '>', $commit_msg or croak $!;
|
||||
|
||||
chomp(my $type = `git-cat-file -t $commit`);
|
||||
if ($type eq 'commit') {
|
||||
my $pid = open my $msg_fh, '-|';
|
||||
defined $pid or croak $!;
|
||||
|
||||
if ($pid == 0) {
|
||||
exec(qw(git-cat-file commit), $commit) or croak $!;
|
||||
}
|
||||
my $in_msg = 0;
|
||||
while (<$msg_fh>) {
|
||||
if (!$in_msg) {
|
||||
$in_msg = 1 if (/^\s*$/);
|
||||
} else {
|
||||
print $msg $_ or croak $!;
|
||||
}
|
||||
}
|
||||
close $msg_fh or croak $!;
|
||||
}
|
||||
close $msg or croak $!;
|
||||
|
||||
if ($_edit || ($type eq 'tree')) {
|
||||
my $editor = $ENV{VISUAL} || $ENV{EDITOR} || 'vi';
|
||||
system($editor, $commit_msg);
|
||||
}
|
||||
my @ci_output = safe_qx(qw(svn commit -F),$commit_msg);
|
||||
my ($committed) = grep(/^Committed revision \d+\./,@ci_output);
|
||||
unlink $commit_msg;
|
||||
defined $committed or croak
|
||||
"Commit output failed to parse committed revision!\n",
|
||||
join("\n",@ci_output),"\n";
|
||||
my ($rev_committed) = ($committed =~ /^Committed revision (\d+)\./);
|
||||
|
||||
# resync immediately
|
||||
my @svn_up = (qw(svn up), "-r$svn_rev");
|
||||
push @svn_up, '--ignore-externals' unless $_no_ignore_ext;
|
||||
sys(@svn_up);
|
||||
return fetch("$rev_committed=$commit")->{revision};
|
||||
}
|
||||
|
||||
sub svn_log_xml {
|
||||
my (@log_args) = @_;
|
||||
my $log_fh = IO::File->new_tmpfile or croak $!;
|
||||
|
||||
my $pid = fork;
|
||||
defined $pid or croak $!;
|
||||
|
||||
if ($pid == 0) {
|
||||
open STDOUT, '>&', $log_fh or croak $!;
|
||||
exec (qw(svn log --xml), @log_args) or croak $!
|
||||
}
|
||||
|
||||
waitpid $pid, 0;
|
||||
croak $? if $?;
|
||||
|
||||
seek $log_fh, 0, 0;
|
||||
my @svn_log;
|
||||
my $log = XML::Simple::XMLin( $log_fh,
|
||||
ForceArray => ['path','revision','logentry'],
|
||||
KeepRoot => 0,
|
||||
KeyAttr => { logentry => '+revision',
|
||||
paths => '+path' },
|
||||
)->{logentry};
|
||||
foreach my $r (sort {$a <=> $b} keys %$log) {
|
||||
my $log_msg = $log->{$r};
|
||||
my ($Y,$m,$d,$H,$M,$S) = ($log_msg->{date} =~
|
||||
/(\d{4})\-(\d\d)\-(\d\d)T
|
||||
(\d\d)\:(\d\d)\:(\d\d)\.\d+Z$/x)
|
||||
or croak "Failed to parse date: ",
|
||||
$log->{$r}->{date};
|
||||
$log_msg->{date} = "+0000 $Y-$m-$d $H:$M:$S";
|
||||
|
||||
# XML::Simple can't handle <msg></msg> as a string:
|
||||
if (ref $log_msg->{msg} eq 'HASH') {
|
||||
$log_msg->{msg} = "\n";
|
||||
} else {
|
||||
$log_msg->{msg} .= "\n";
|
||||
}
|
||||
push @svn_log, $log->{$r};
|
||||
}
|
||||
return \@svn_log;
|
||||
}
|
||||
|
||||
sub svn_log_raw {
|
||||
my (@log_args) = @_;
|
||||
my $pid = open my $log_fh,'-|';
|
||||
defined $pid or croak $!;
|
||||
|
||||
if ($pid == 0) {
|
||||
exec (qw(svn log), @log_args) or croak $!
|
||||
}
|
||||
|
||||
my @svn_log;
|
||||
my $state;
|
||||
while (<$log_fh>) {
|
||||
chomp;
|
||||
if (/^\-{72}$/) {
|
||||
$state = 'rev';
|
||||
|
||||
# if we have an empty log message, put something there:
|
||||
if (@svn_log) {
|
||||
$svn_log[$#svn_log]->{msg} ||= "\n";
|
||||
}
|
||||
next;
|
||||
}
|
||||
if ($state eq 'rev' && s/^r(\d+)\s*\|\s*//) {
|
||||
my $rev = $1;
|
||||
my ($author, $date) = split(/\s*\|\s*/, $_, 2);
|
||||
my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~
|
||||
/(\d{4})\-(\d\d)\-(\d\d)\s
|
||||
(\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x)
|
||||
or croak "Failed to parse date: $date\n";
|
||||
my %log_msg = ( revision => $rev,
|
||||
date => "$tz $Y-$m-$d $H:$M:$S",
|
||||
author => $author,
|
||||
msg => '' );
|
||||
push @svn_log, \%log_msg;
|
||||
$state = 'msg_start';
|
||||
next;
|
||||
}
|
||||
# skip the first blank line of the message:
|
||||
if ($state eq 'msg_start' && /^$/) {
|
||||
$state = 'msg';
|
||||
} elsif ($state eq 'msg') {
|
||||
$svn_log[$#svn_log]->{msg} .= $_."\n";
|
||||
}
|
||||
}
|
||||
close $log_fh or croak $?;
|
||||
return \@svn_log;
|
||||
}
|
||||
|
||||
sub svn_info {
|
||||
my $url = shift || $SVN_URL;
|
||||
|
||||
my $pid = open my $info_fh, '-|';
|
||||
defined $pid or croak $!;
|
||||
|
||||
if ($pid == 0) {
|
||||
exec(qw(svn info),$url) or croak $!;
|
||||
}
|
||||
|
||||
my $ret = {};
|
||||
# only single-lines seem to exist in svn info output
|
||||
while (<$info_fh>) {
|
||||
chomp $_;
|
||||
if (m#^([^:]+)\s*:\s*(\S*)$#) {
|
||||
$ret->{$1} = $2;
|
||||
push @{$ret->{-order}}, $1;
|
||||
}
|
||||
}
|
||||
close $info_fh or croak $!;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
sub sys { system(@_) == 0 or croak $? }
|
||||
|
||||
sub git_addremove {
|
||||
system( "git-ls-files -z --others ".
|
||||
"'--exclude-from=$GIT_DIR/$GIT_SVN/info/exclude'".
|
||||
"| git-update-index --add -z --stdin; ".
|
||||
"git-ls-files -z --deleted ".
|
||||
"| git-update-index --remove -z --stdin; ".
|
||||
"git-ls-files -z --modified".
|
||||
"| git-update-index -z --stdin") == 0 or croak $?
|
||||
}
|
||||
|
||||
sub s_to_file {
|
||||
my ($str, $file, $mode) = @_;
|
||||
open my $fd,'>',$file or croak $!;
|
||||
print $fd $str,"\n" or croak $!;
|
||||
close $fd or croak $!;
|
||||
chmod ($mode &~ umask, $file) if (defined $mode);
|
||||
}
|
||||
|
||||
sub file_to_s {
|
||||
my $file = shift;
|
||||
open my $fd,'<',$file or croak "$!: file: $file\n";
|
||||
local $/;
|
||||
my $ret = <$fd>;
|
||||
close $fd or croak $!;
|
||||
$ret =~ s/\s*$//s;
|
||||
return $ret;
|
||||
}
|
||||
|
||||
sub assert_revision_unknown {
|
||||
my $revno = shift;
|
||||
if (-f "$REV_DIR/$revno") {
|
||||
croak "$REV_DIR/$revno already exists! ",
|
||||
"Why are we refetching it?";
|
||||
}
|
||||
}
|
||||
|
||||
sub assert_revision_eq_or_unknown {
|
||||
my ($revno, $commit) = @_;
|
||||
if (-f "$REV_DIR/$revno") {
|
||||
my $current = file_to_s("$REV_DIR/$revno");
|
||||
if ($commit ne $current) {
|
||||
croak "$REV_DIR/$revno already exists!\n",
|
||||
"current: $current\nexpected: $commit\n";
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sub git_commit {
|
||||
my ($log_msg, @parents) = @_;
|
||||
assert_revision_unknown($log_msg->{revision});
|
||||
my $out_fh = IO::File->new_tmpfile or croak $!;
|
||||
my $info = svn_info('.');
|
||||
my $uuid = $info->{'Repository UUID'};
|
||||
defined $uuid or croak "Unable to get Repository UUID\n";
|
||||
|
||||
# commit parents can be conditionally bound to a particular
|
||||
# svn revision via: "svn_revno=commit_sha1", filter them out here:
|
||||
my @exec_parents;
|
||||
foreach my $p (@parents) {
|
||||
next unless defined $p;
|
||||
if ($p =~ /^(\d+)=($sha1_short)$/o) {
|
||||
if ($1 == $log_msg->{revision}) {
|
||||
push @exec_parents, $2;
|
||||
}
|
||||
} else {
|
||||
push @exec_parents, $p if $p =~ /$sha1_short/o;
|
||||
}
|
||||
}
|
||||
|
||||
my $pid = fork;
|
||||
defined $pid or croak $!;
|
||||
if ($pid == 0) {
|
||||
$ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX;
|
||||
git_addremove();
|
||||
chomp(my $tree = `git-write-tree`);
|
||||
croak if $?;
|
||||
my $msg_fh = IO::File->new_tmpfile or croak $!;
|
||||
print $msg_fh $log_msg->{msg}, "\ngit-svn-id: ",
|
||||
"$SVN_URL\@$log_msg->{revision}",
|
||||
" $uuid\n" or croak $!;
|
||||
$msg_fh->flush == 0 or croak $!;
|
||||
seek $msg_fh, 0, 0 or croak $!;
|
||||
|
||||
$ENV{GIT_AUTHOR_NAME} = $ENV{GIT_COMMITTER_NAME} =
|
||||
$log_msg->{author};
|
||||
$ENV{GIT_AUTHOR_EMAIL} = $ENV{GIT_COMMITTER_EMAIL} =
|
||||
$log_msg->{author}."\@$uuid";
|
||||
$ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} =
|
||||
$log_msg->{date};
|
||||
my @exec = ('git-commit-tree',$tree);
|
||||
push @exec, '-p', $_ foreach @exec_parents;
|
||||
open STDIN, '<&', $msg_fh or croak $!;
|
||||
open STDOUT, '>&', $out_fh or croak $!;
|
||||
exec @exec or croak $!;
|
||||
}
|
||||
waitpid($pid,0);
|
||||
croak if $?;
|
||||
|
||||
$out_fh->flush == 0 or croak $!;
|
||||
seek $out_fh, 0, 0 or croak $!;
|
||||
chomp(my $commit = do { local $/; <$out_fh> });
|
||||
if ($commit !~ /^$sha1$/o) {
|
||||
croak "Failed to commit, invalid sha1: $commit\n";
|
||||
}
|
||||
my @update_ref = ('git-update-ref',"refs/heads/$GIT_SVN-HEAD",$commit);
|
||||
if (my $primary_parent = shift @exec_parents) {
|
||||
push @update_ref, $primary_parent;
|
||||
}
|
||||
sys(@update_ref);
|
||||
sys('git-update-ref',"$GIT_SVN/revs/$log_msg->{revision}",$commit);
|
||||
print "r$log_msg->{revision} = $commit\n";
|
||||
return $commit;
|
||||
}
|
||||
|
||||
sub blob_to_symlink {
|
||||
my ($blob, $link) = @_;
|
||||
defined $link or croak "\$link not defined!\n";
|
||||
croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
|
||||
my $dest = `git-cat-file blob $blob`; # no newline, so no chomp
|
||||
symlink $dest, $link or croak $!;
|
||||
}
|
||||
|
||||
sub blob_to_file {
|
||||
my ($blob, $file) = @_;
|
||||
defined $file or croak "\$file not defined!\n";
|
||||
croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o;
|
||||
open my $blob_fh, '>', $file or croak "$!: $file\n";
|
||||
my $pid = fork;
|
||||
defined $pid or croak $!;
|
||||
|
||||
if ($pid == 0) {
|
||||
open STDOUT, '>&', $blob_fh or croak $!;
|
||||
exec('git-cat-file','blob',$blob);
|
||||
}
|
||||
waitpid $pid, 0;
|
||||
croak $? if $?;
|
||||
|
||||
close $blob_fh or croak $!;
|
||||
}
|
||||
|
||||
sub safe_qx {
|
||||
my $pid = open my $child, '-|';
|
||||
defined $pid or croak $!;
|
||||
if ($pid == 0) {
|
||||
exec(@_) or croak $?;
|
||||
}
|
||||
my @ret = (<$child>);
|
||||
close $child or croak $?;
|
||||
die $? if $?; # just in case close didn't error out
|
||||
return wantarray ? @ret : join('',@ret);
|
||||
}
|
||||
|
||||
sub svn_check_ignore_externals {
|
||||
return if $_no_ignore_ext;
|
||||
unless (grep /ignore-externals/,(safe_qx(qw(svn co -h)))) {
|
||||
print STDERR "W: Installed svn version does not support ",
|
||||
"--ignore-externals\n";
|
||||
$_no_ignore_ext = 1;
|
||||
}
|
||||
}
|
||||
__END__
|
||||
|
||||
Data structures:
|
||||
|
||||
@svn_log = array of log_msg hashes
|
||||
|
||||
$log_msg hash
|
||||
{
|
||||
msg => 'whitespace-formatted log entry
|
||||
', # trailing newline is preserved
|
||||
revision => '8', # integer
|
||||
date => '2004-02-24T17:01:44.108345Z', # commit date
|
||||
author => 'committer name'
|
||||
};
|
||||
|
||||
|
||||
@mods = array of diff-index line hashes, each element represents one line
|
||||
of diff-index output
|
||||
|
||||
diff-index line ($m hash)
|
||||
{
|
||||
mode_a => first column of diff-index output, no leading ':',
|
||||
mode_b => second column of diff-index output,
|
||||
sha1_b => sha1sum of the final blob,
|
||||
chg => change type [MCRAD],
|
||||
file_a => original file name of a file (iff chg is 'C' or 'R')
|
||||
file_b => new/current file name of a file (any chg)
|
||||
}
|
||||
;
|
208
contrib/git-svn/git-svn.txt
Normal file
208
contrib/git-svn/git-svn.txt
Normal file
|
@ -0,0 +1,208 @@
|
|||
git-svn(1)
|
||||
==========
|
||||
|
||||
NAME
|
||||
----
|
||||
git-svn - bidirectional operation between a single Subversion branch and git
|
||||
|
||||
SYNOPSIS
|
||||
--------
|
||||
'git-svn' <command> [options] [arguments]
|
||||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
git-svn is a simple conduit for changesets between a single Subversion
|
||||
branch and git.
|
||||
|
||||
git-svn is not to be confused with git-svnimport. The were designed
|
||||
with very different goals in mind.
|
||||
|
||||
git-svn is designed for an individual developer who wants a
|
||||
bidirectional flow of changesets between a single branch in Subversion
|
||||
and an arbitrary number of branches in git. git-svnimport is designed
|
||||
for read-only operation on repositories that match a particular layout
|
||||
(albeit the recommended one by SVN developers).
|
||||
|
||||
For importing svn, git-svnimport is potentially more powerful when
|
||||
operating on repositories organized under the recommended
|
||||
trunk/branch/tags structure, and should be faster, too.
|
||||
|
||||
git-svn completely ignores the very limited view of branching that
|
||||
Subversion has. This allows git-svn to be much easier to use,
|
||||
especially on repositories that are not organized in a manner that
|
||||
git-svnimport is designed for.
|
||||
|
||||
COMMANDS
|
||||
--------
|
||||
init::
|
||||
Creates an empty git repository with additional metadata
|
||||
directories for git-svn. The SVN_URL must be specified
|
||||
at this point.
|
||||
|
||||
fetch::
|
||||
Fetch unfetched revisions from the SVN_URL we are tracking.
|
||||
refs/heads/git-svn-HEAD will be updated to the latest revision.
|
||||
|
||||
commit::
|
||||
Commit specified commit or tree objects to SVN. This relies on
|
||||
your imported fetch data being up-to-date. This makes
|
||||
absolutely no attempts to do patching when committing to SVN, it
|
||||
simply overwrites files with those specified in the tree or
|
||||
commit. All merging is assumed to have taken place
|
||||
independently of git-svn functions.
|
||||
|
||||
rebuild::
|
||||
Not a part of daily usage, but this is a useful command if
|
||||
you've just cloned a repository (using git-clone) that was
|
||||
tracked with git-svn. Unfortunately, git-clone does not clone
|
||||
git-svn metadata and the svn working tree that git-svn uses for
|
||||
its operations. This rebuilds the metadata so git-svn can
|
||||
resume fetch operations. SVN_URL may be optionally specified if
|
||||
the directory/repository you're tracking has moved or changed
|
||||
protocols.
|
||||
|
||||
OPTIONS
|
||||
-------
|
||||
-r <ARG>::
|
||||
--revision <ARG>::
|
||||
Only used with the 'fetch' command.
|
||||
|
||||
Takes any valid -r<argument> svn would accept and passes it
|
||||
directly to svn. -r<ARG1>:<ARG2> ranges and "{" DATE "}" syntax
|
||||
is also supported. This is passed directly to svn, see svn
|
||||
documentation for more details.
|
||||
|
||||
This can allow you to make partial mirrors when running fetch.
|
||||
|
||||
-::
|
||||
--stdin::
|
||||
Only used with the 'commit' command.
|
||||
|
||||
Read a list of commits from stdin and commit them in reverse
|
||||
order. Only the leading sha1 is read from each line, so
|
||||
git-rev-list --pretty=oneline output can be used.
|
||||
|
||||
--rmdir::
|
||||
Only used with the 'commit' command.
|
||||
|
||||
Remove directories from the SVN tree if there are no files left
|
||||
behind. SVN can version empty directories, and they are not
|
||||
removed by default if there are no files left in them. git
|
||||
cannot version empty directories. Enabling this flag will make
|
||||
the commit to SVN act like git.
|
||||
|
||||
-e::
|
||||
--edit::
|
||||
Only used with the 'commit' command.
|
||||
|
||||
Edit the commit message before committing to SVN. This is off by
|
||||
default for objects that are commits, and forced on when committing
|
||||
tree objects.
|
||||
|
||||
COMPATIBILITY OPTIONS
|
||||
---------------------
|
||||
--no-ignore-externals::
|
||||
Only used with the 'fetch' and 'rebuild' command.
|
||||
|
||||
By default, git-svn passes --ignore-externals to svn to avoid
|
||||
fetching svn:external trees into git. Pass this flag to enable
|
||||
externals tracking directly via git.
|
||||
|
||||
Versions of svn that do not support --ignore-externals are
|
||||
automatically detected and this flag will be automatically
|
||||
enabled for them.
|
||||
|
||||
Otherwise, do not enable this flag unless you know what you're
|
||||
doing.
|
||||
|
||||
--no-stop-on-copy::
|
||||
Only used with the 'fetch' command.
|
||||
|
||||
By default, git-svn passes --stop-on-copy to avoid dealing with
|
||||
the copied/renamed branch directory problem entirely. A
|
||||
copied/renamed branch is the result of a <SVN_URL> being created
|
||||
in the past from a different source. These are problematic to
|
||||
deal with even when working purely with svn if you work inside
|
||||
subdirectories.
|
||||
|
||||
Do not use this flag unless you know exactly what you're getting
|
||||
yourself into. You have been warned.
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
Tracking and contributing to an Subversion managed-project:
|
||||
|
||||
# Initialize a tree (like git init-db)::
|
||||
git-svn init http://svn.foo.org/project/trunk
|
||||
# Fetch remote revisions::
|
||||
git-svn fetch
|
||||
# Create your own branch to hack on::
|
||||
git checkout -b my-branch git-svn-HEAD
|
||||
# Commit only the git commits you want to SVN::
|
||||
git-svn commit <tree-ish> [<tree-ish_2> ...]
|
||||
# Commit all the git commits from my-branch that don't exist in SVN::
|
||||
git rev-list --pretty=oneline git-svn-HEAD..my-branch | git-svn commit
|
||||
# Something is committed to SVN, pull the latest into your branch::
|
||||
git-svn fetch && git pull . git-svn-HEAD
|
||||
|
||||
DESIGN PHILOSOPHY
|
||||
-----------------
|
||||
Merge tracking in Subversion is lacking and doing branched development
|
||||
with Subversion is cumbersome as a result. git-svn completely forgoes
|
||||
any automated merge/branch tracking on the Subversion side and leaves it
|
||||
entirely up to the user on the git side. It's simply not worth it to do
|
||||
a useful translation when the the original signal is weak.
|
||||
|
||||
TRACKING MULTIPLE REPOSITORIES OR BRANCHES
|
||||
------------------------------------------
|
||||
This is for advanced users, most users should ignore this section.
|
||||
|
||||
Because git-svn does not care about relationships between different
|
||||
branches or directories in a Subversion repository, git-svn has a simple
|
||||
hack to allow it to track an arbitrary number of related _or_ unrelated
|
||||
SVN repositories via one git repository. Simply set the GIT_SVN_ID
|
||||
environment variable to a name other other than "git-svn" (the default)
|
||||
and git-svn will ignore the contents of the $GIT_DIR/git-svn directory
|
||||
and instead do all of its work in $GIT_DIR/$GIT_SVN_ID for that
|
||||
invocation.
|
||||
|
||||
ADDITIONAL FETCH ARGUMENTS
|
||||
--------------------------
|
||||
This is for advanced users, most users should ignore this section.
|
||||
|
||||
Unfetched SVN revisions may be imported as children of existing commits
|
||||
by specifying additional arguments to 'fetch'. Additional parents may
|
||||
optionally be specified in the form of sha1 hex sums at the
|
||||
command-line. Unfetched SVN revisions may also be tied to particular
|
||||
git commits with the following syntax:
|
||||
|
||||
svn_revision_number=git_commit_sha1
|
||||
|
||||
This allows you to tie unfetched SVN revision 375 to your current HEAD::
|
||||
|
||||
git-svn fetch 375=$(git-rev-parse HEAD)
|
||||
|
||||
BUGS
|
||||
----
|
||||
If somebody commits a conflicting changeset to SVN at a bad moment
|
||||
(right before you commit) causing a conflict and your commit to fail,
|
||||
your svn working tree ($GIT_DIR/git-svn/tree) may be dirtied. The
|
||||
easiest thing to do is probably just to rm -rf $GIT_DIR/git-svn/tree and
|
||||
run 'rebuild'.
|
||||
|
||||
We ignore all SVN properties except svn:executable. Too difficult to
|
||||
map them since we rely heavily on git write-tree being _exactly_ the
|
||||
same on both the SVN and git working trees and I prefer not to clutter
|
||||
working trees with metadata files.
|
||||
|
||||
svn:keywords can't be ignored in Subversion (at least I don't know of
|
||||
a way to ignore them).
|
||||
|
||||
Author
|
||||
------
|
||||
Written by Eric Wong <normalperson@yhbt.net>.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
Written by Eric Wong <normalperson@yhbt.net>.
|
992
contrib/gitview/gitview
Executable file
992
contrib/gitview/gitview
Executable file
|
@ -0,0 +1,992 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
""" gitview
|
||||
GUI browser for git repository
|
||||
This program is based on bzrk by Scott James Remnant <scott@ubuntu.com>
|
||||
"""
|
||||
__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P."
|
||||
__author__ = "Aneesh Kumar K.V <aneesh.kumar@hp.com>"
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import gtk
|
||||
import pygtk
|
||||
import pango
|
||||
import re
|
||||
import time
|
||||
import gobject
|
||||
import cairo
|
||||
import math
|
||||
import string
|
||||
|
||||
try:
|
||||
import gtksourceview
|
||||
have_gtksourceview = True
|
||||
except ImportError:
|
||||
have_gtksourceview = False
|
||||
print "Running without gtksourceview module"
|
||||
|
||||
re_ident = re.compile('(author|committer) (?P<ident>.*) (?P<epoch>\d+) (?P<tz>[+-]\d{4})')
|
||||
|
||||
def list_to_string(args, skip):
|
||||
count = len(args)
|
||||
i = skip
|
||||
str_arg=" "
|
||||
while (i < count ):
|
||||
str_arg = str_arg + args[i]
|
||||
str_arg = str_arg + " "
|
||||
i = i+1
|
||||
|
||||
return str_arg
|
||||
|
||||
def show_date(epoch, tz):
|
||||
secs = float(epoch)
|
||||
tzsecs = float(tz[1:3]) * 3600
|
||||
tzsecs += float(tz[3:5]) * 60
|
||||
if (tz[0] == "+"):
|
||||
secs += tzsecs
|
||||
else:
|
||||
secs -= tzsecs
|
||||
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(secs))
|
||||
|
||||
def get_sha1_from_tags(line):
|
||||
fp = os.popen("git cat-file -t " + line)
|
||||
entry = string.strip(fp.readline())
|
||||
fp.close()
|
||||
if (entry == "commit"):
|
||||
return line
|
||||
elif (entry == "tag"):
|
||||
fp = os.popen("git cat-file tag "+ line)
|
||||
entry = string.strip(fp.readline())
|
||||
fp.close()
|
||||
obj = re.split(" ", entry)
|
||||
if (obj[0] == "object"):
|
||||
return obj[1]
|
||||
return None
|
||||
|
||||
class CellRendererGraph(gtk.GenericCellRenderer):
|
||||
"""Cell renderer for directed graph.
|
||||
|
||||
This module contains the implementation of a custom GtkCellRenderer that
|
||||
draws part of the directed graph based on the lines suggested by the code
|
||||
in graph.py.
|
||||
|
||||
Because we're shiny, we use Cairo to do this, and because we're naughty
|
||||
we cheat and draw over the bits of the TreeViewColumn that are supposed to
|
||||
just be for the background.
|
||||
|
||||
Properties:
|
||||
node (column, colour, [ names ]) tuple to draw revision node,
|
||||
in_lines (start, end, colour) tuple list to draw inward lines,
|
||||
out_lines (start, end, colour) tuple list to draw outward lines.
|
||||
"""
|
||||
|
||||
__gproperties__ = {
|
||||
"node": ( gobject.TYPE_PYOBJECT, "node",
|
||||
"revision node instruction",
|
||||
gobject.PARAM_WRITABLE
|
||||
),
|
||||
"in-lines": ( gobject.TYPE_PYOBJECT, "in-lines",
|
||||
"instructions to draw lines into the cell",
|
||||
gobject.PARAM_WRITABLE
|
||||
),
|
||||
"out-lines": ( gobject.TYPE_PYOBJECT, "out-lines",
|
||||
"instructions to draw lines out of the cell",
|
||||
gobject.PARAM_WRITABLE
|
||||
),
|
||||
}
|
||||
|
||||
def do_set_property(self, property, value):
|
||||
"""Set properties from GObject properties."""
|
||||
if property.name == "node":
|
||||
self.node = value
|
||||
elif property.name == "in-lines":
|
||||
self.in_lines = value
|
||||
elif property.name == "out-lines":
|
||||
self.out_lines = value
|
||||
else:
|
||||
raise AttributeError, "no such property: '%s'" % property.name
|
||||
|
||||
def box_size(self, widget):
|
||||
"""Calculate box size based on widget's font.
|
||||
|
||||
Cache this as it's probably expensive to get. It ensures that we
|
||||
draw the graph at least as large as the text.
|
||||
"""
|
||||
try:
|
||||
return self._box_size
|
||||
except AttributeError:
|
||||
pango_ctx = widget.get_pango_context()
|
||||
font_desc = widget.get_style().font_desc
|
||||
metrics = pango_ctx.get_metrics(font_desc)
|
||||
|
||||
ascent = pango.PIXELS(metrics.get_ascent())
|
||||
descent = pango.PIXELS(metrics.get_descent())
|
||||
|
||||
self._box_size = ascent + descent + 6
|
||||
return self._box_size
|
||||
|
||||
def set_colour(self, ctx, colour, bg, fg):
|
||||
"""Set the context source colour.
|
||||
|
||||
Picks a distinct colour based on an internal wheel; the bg
|
||||
parameter provides the value that should be assigned to the 'zero'
|
||||
colours and the fg parameter provides the multiplier that should be
|
||||
applied to the foreground colours.
|
||||
"""
|
||||
colours = [
|
||||
( 1.0, 0.0, 0.0 ),
|
||||
( 1.0, 1.0, 0.0 ),
|
||||
( 0.0, 1.0, 0.0 ),
|
||||
( 0.0, 1.0, 1.0 ),
|
||||
( 0.0, 0.0, 1.0 ),
|
||||
( 1.0, 0.0, 1.0 ),
|
||||
]
|
||||
|
||||
colour %= len(colours)
|
||||
red = (colours[colour][0] * fg) or bg
|
||||
green = (colours[colour][1] * fg) or bg
|
||||
blue = (colours[colour][2] * fg) or bg
|
||||
|
||||
ctx.set_source_rgb(red, green, blue)
|
||||
|
||||
def on_get_size(self, widget, cell_area):
|
||||
"""Return the size we need for this cell.
|
||||
|
||||
Each cell is drawn individually and is only as wide as it needs
|
||||
to be, we let the TreeViewColumn take care of making them all
|
||||
line up.
|
||||
"""
|
||||
box_size = self.box_size(widget)
|
||||
|
||||
cols = self.node[0]
|
||||
for start, end, colour in self.in_lines + self.out_lines:
|
||||
cols = max(cols, start, end)
|
||||
|
||||
(column, colour, names) = self.node
|
||||
names_len = 0
|
||||
if (len(names) != 0):
|
||||
for item in names:
|
||||
names_len += len(item)/3
|
||||
|
||||
width = box_size * (cols + 1 + names_len )
|
||||
height = box_size
|
||||
|
||||
# FIXME I have no idea how to use cell_area properly
|
||||
return (0, 0, width, height)
|
||||
|
||||
def on_render(self, window, widget, bg_area, cell_area, exp_area, flags):
|
||||
"""Render an individual cell.
|
||||
|
||||
Draws the cell contents using cairo, taking care to clip what we
|
||||
do to within the background area so we don't draw over other cells.
|
||||
Note that we're a bit naughty there and should really be drawing
|
||||
in the cell_area (or even the exposed area), but we explicitly don't
|
||||
want any gutter.
|
||||
|
||||
We try and be a little clever, if the line we need to draw is going
|
||||
to cross other columns we actually draw it as in the .---' style
|
||||
instead of a pure diagonal ... this reduces confusion by an
|
||||
incredible amount.
|
||||
"""
|
||||
ctx = window.cairo_create()
|
||||
ctx.rectangle(bg_area.x, bg_area.y, bg_area.width, bg_area.height)
|
||||
ctx.clip()
|
||||
|
||||
box_size = self.box_size(widget)
|
||||
|
||||
ctx.set_line_width(box_size / 8)
|
||||
ctx.set_line_cap(cairo.LINE_CAP_SQUARE)
|
||||
|
||||
# Draw lines into the cell
|
||||
for start, end, colour in self.in_lines:
|
||||
ctx.move_to(cell_area.x + box_size * start + box_size / 2,
|
||||
bg_area.y - bg_area.height / 2)
|
||||
|
||||
if start - end > 1:
|
||||
ctx.line_to(cell_area.x + box_size * start, bg_area.y)
|
||||
ctx.line_to(cell_area.x + box_size * end + box_size, bg_area.y)
|
||||
elif start - end < -1:
|
||||
ctx.line_to(cell_area.x + box_size * start + box_size,
|
||||
bg_area.y)
|
||||
ctx.line_to(cell_area.x + box_size * end, bg_area.y)
|
||||
|
||||
ctx.line_to(cell_area.x + box_size * end + box_size / 2,
|
||||
bg_area.y + bg_area.height / 2)
|
||||
|
||||
self.set_colour(ctx, colour, 0.0, 0.65)
|
||||
ctx.stroke()
|
||||
|
||||
# Draw lines out of the cell
|
||||
for start, end, colour in self.out_lines:
|
||||
ctx.move_to(cell_area.x + box_size * start + box_size / 2,
|
||||
bg_area.y + bg_area.height / 2)
|
||||
|
||||
if start - end > 1:
|
||||
ctx.line_to(cell_area.x + box_size * start,
|
||||
bg_area.y + bg_area.height)
|
||||
ctx.line_to(cell_area.x + box_size * end + box_size,
|
||||
bg_area.y + bg_area.height)
|
||||
elif start - end < -1:
|
||||
ctx.line_to(cell_area.x + box_size * start + box_size,
|
||||
bg_area.y + bg_area.height)
|
||||
ctx.line_to(cell_area.x + box_size * end,
|
||||
bg_area.y + bg_area.height)
|
||||
|
||||
ctx.line_to(cell_area.x + box_size * end + box_size / 2,
|
||||
bg_area.y + bg_area.height / 2 + bg_area.height)
|
||||
|
||||
self.set_colour(ctx, colour, 0.0, 0.65)
|
||||
ctx.stroke()
|
||||
|
||||
# Draw the revision node in the right column
|
||||
(column, colour, names) = self.node
|
||||
ctx.arc(cell_area.x + box_size * column + box_size / 2,
|
||||
cell_area.y + cell_area.height / 2,
|
||||
box_size / 4, 0, 2 * math.pi)
|
||||
|
||||
|
||||
if (len(names) != 0):
|
||||
name = " "
|
||||
for item in names:
|
||||
name = name + item + " "
|
||||
|
||||
ctx.text_path(name)
|
||||
|
||||
self.set_colour(ctx, colour, 0.0, 0.5)
|
||||
ctx.stroke_preserve()
|
||||
|
||||
self.set_colour(ctx, colour, 0.5, 1.0)
|
||||
ctx.fill()
|
||||
|
||||
class Commit:
|
||||
""" This represent a commit object obtained after parsing the git-rev-list
|
||||
output """
|
||||
|
||||
children_sha1 = {}
|
||||
|
||||
def __init__(self, commit_lines):
|
||||
self.message = ""
|
||||
self.author = ""
|
||||
self.date = ""
|
||||
self.committer = ""
|
||||
self.commit_date = ""
|
||||
self.commit_sha1 = ""
|
||||
self.parent_sha1 = [ ]
|
||||
self.parse_commit(commit_lines)
|
||||
|
||||
|
||||
def parse_commit(self, commit_lines):
|
||||
|
||||
# First line is the sha1 lines
|
||||
line = string.strip(commit_lines[0])
|
||||
sha1 = re.split(" ", line)
|
||||
self.commit_sha1 = sha1[0]
|
||||
self.parent_sha1 = sha1[1:]
|
||||
|
||||
#build the child list
|
||||
for parent_id in self.parent_sha1:
|
||||
try:
|
||||
Commit.children_sha1[parent_id].append(self.commit_sha1)
|
||||
except KeyError:
|
||||
Commit.children_sha1[parent_id] = [self.commit_sha1]
|
||||
|
||||
# IF we don't have parent
|
||||
if (len(self.parent_sha1) == 0):
|
||||
self.parent_sha1 = [0]
|
||||
|
||||
for line in commit_lines[1:]:
|
||||
m = re.match("^ ", line)
|
||||
if (m != None):
|
||||
# First line of the commit message used for short log
|
||||
if self.message == "":
|
||||
self.message = string.strip(line)
|
||||
continue
|
||||
|
||||
m = re.match("tree", line)
|
||||
if (m != None):
|
||||
continue
|
||||
|
||||
m = re.match("parent", line)
|
||||
if (m != None):
|
||||
continue
|
||||
|
||||
m = re_ident.match(line)
|
||||
if (m != None):
|
||||
date = show_date(m.group('epoch'), m.group('tz'))
|
||||
if m.group(1) == "author":
|
||||
self.author = m.group('ident')
|
||||
self.date = date
|
||||
elif m.group(1) == "committer":
|
||||
self.committer = m.group('ident')
|
||||
self.commit_date = date
|
||||
|
||||
continue
|
||||
|
||||
def get_message(self, with_diff=0):
|
||||
if (with_diff == 1):
|
||||
message = self.diff_tree()
|
||||
else:
|
||||
fp = os.popen("git cat-file commit " + self.commit_sha1)
|
||||
message = fp.read()
|
||||
fp.close()
|
||||
|
||||
return message
|
||||
|
||||
def diff_tree(self):
|
||||
fp = os.popen("git diff-tree --pretty --cc -v -p --always " + self.commit_sha1)
|
||||
diff = fp.read()
|
||||
fp.close()
|
||||
return diff
|
||||
|
||||
class DiffWindow:
|
||||
"""Diff window.
|
||||
This object represents and manages a single window containing the
|
||||
differences between two revisions on a branch.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||||
self.window.set_border_width(0)
|
||||
self.window.set_title("Git repository browser diff window")
|
||||
|
||||
# Use two thirds of the screen by default
|
||||
screen = self.window.get_screen()
|
||||
monitor = screen.get_monitor_geometry(0)
|
||||
width = int(monitor.width * 0.66)
|
||||
height = int(monitor.height * 0.66)
|
||||
self.window.set_default_size(width, height)
|
||||
|
||||
self.construct()
|
||||
|
||||
def construct(self):
|
||||
"""Construct the window contents."""
|
||||
vbox = gtk.VBox()
|
||||
self.window.add(vbox)
|
||||
vbox.show()
|
||||
|
||||
menu_bar = gtk.MenuBar()
|
||||
save_menu = gtk.ImageMenuItem(gtk.STOCK_SAVE)
|
||||
save_menu.connect("activate", self.save_menu_response, "save")
|
||||
save_menu.show()
|
||||
menu_bar.append(save_menu)
|
||||
vbox.pack_start(menu_bar, False, False, 2)
|
||||
menu_bar.show()
|
||||
|
||||
scrollwin = gtk.ScrolledWindow()
|
||||
scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
||||
scrollwin.set_shadow_type(gtk.SHADOW_IN)
|
||||
vbox.pack_start(scrollwin, expand=True, fill=True)
|
||||
scrollwin.show()
|
||||
|
||||
if have_gtksourceview:
|
||||
self.buffer = gtksourceview.SourceBuffer()
|
||||
slm = gtksourceview.SourceLanguagesManager()
|
||||
gsl = slm.get_language_from_mime_type("text/x-patch")
|
||||
self.buffer.set_highlight(True)
|
||||
self.buffer.set_language(gsl)
|
||||
sourceview = gtksourceview.SourceView(self.buffer)
|
||||
else:
|
||||
self.buffer = gtk.TextBuffer()
|
||||
sourceview = gtk.TextView(self.buffer)
|
||||
|
||||
sourceview.set_editable(False)
|
||||
sourceview.modify_font(pango.FontDescription("Monospace"))
|
||||
scrollwin.add(sourceview)
|
||||
sourceview.show()
|
||||
|
||||
|
||||
def set_diff(self, commit_sha1, parent_sha1):
|
||||
"""Set the differences showed by this window.
|
||||
Compares the two trees and populates the window with the
|
||||
differences.
|
||||
"""
|
||||
# Diff with the first commit or the last commit shows nothing
|
||||
if (commit_sha1 == 0 or parent_sha1 == 0 ):
|
||||
return
|
||||
|
||||
fp = os.popen("git diff-tree -p " + parent_sha1 + " " + commit_sha1)
|
||||
self.buffer.set_text(fp.read())
|
||||
fp.close()
|
||||
self.window.show()
|
||||
|
||||
def save_menu_response(self, widget, string):
|
||||
dialog = gtk.FileChooserDialog("Save..", None, gtk.FILE_CHOOSER_ACTION_SAVE,
|
||||
(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
|
||||
gtk.STOCK_SAVE, gtk.RESPONSE_OK))
|
||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
||||
response = dialog.run()
|
||||
if response == gtk.RESPONSE_OK:
|
||||
patch_buffer = self.buffer.get_text(self.buffer.get_start_iter(),
|
||||
self.buffer.get_end_iter())
|
||||
fp = open(dialog.get_filename(), "w")
|
||||
fp.write(patch_buffer)
|
||||
fp.close()
|
||||
dialog.destroy()
|
||||
|
||||
class GitView:
|
||||
""" This is the main class
|
||||
"""
|
||||
version = "0.6"
|
||||
|
||||
def __init__(self, with_diff=0):
|
||||
self.with_diff = with_diff
|
||||
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||||
self.window.set_border_width(0)
|
||||
self.window.set_title("Git repository browser")
|
||||
|
||||
self.get_bt_sha1()
|
||||
|
||||
# Use three-quarters of the screen by default
|
||||
screen = self.window.get_screen()
|
||||
monitor = screen.get_monitor_geometry(0)
|
||||
width = int(monitor.width * 0.75)
|
||||
height = int(monitor.height * 0.75)
|
||||
self.window.set_default_size(width, height)
|
||||
|
||||
# FIXME AndyFitz!
|
||||
icon = self.window.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
|
||||
self.window.set_icon(icon)
|
||||
|
||||
self.accel_group = gtk.AccelGroup()
|
||||
self.window.add_accel_group(self.accel_group)
|
||||
|
||||
self.construct()
|
||||
|
||||
def get_bt_sha1(self):
|
||||
""" Update the bt_sha1 dictionary with the
|
||||
respective sha1 details """
|
||||
|
||||
self.bt_sha1 = { }
|
||||
git_dir = os.getenv("GIT_DIR")
|
||||
if (git_dir == None):
|
||||
git_dir = ".git"
|
||||
|
||||
#FIXME the path seperator
|
||||
ref_files = os.listdir(git_dir + "/refs/tags")
|
||||
for file in ref_files:
|
||||
fp = open(git_dir + "/refs/tags/"+file)
|
||||
sha1 = get_sha1_from_tags(string.strip(fp.readline()))
|
||||
try:
|
||||
self.bt_sha1[sha1].append(file)
|
||||
except KeyError:
|
||||
self.bt_sha1[sha1] = [file]
|
||||
fp.close()
|
||||
|
||||
|
||||
#FIXME the path seperator
|
||||
ref_files = os.listdir(git_dir + "/refs/heads")
|
||||
for file in ref_files:
|
||||
fp = open(git_dir + "/refs/heads/" + file)
|
||||
sha1 = get_sha1_from_tags(string.strip(fp.readline()))
|
||||
try:
|
||||
self.bt_sha1[sha1].append(file)
|
||||
except KeyError:
|
||||
self.bt_sha1[sha1] = [file]
|
||||
fp.close()
|
||||
|
||||
|
||||
def construct(self):
|
||||
"""Construct the window contents."""
|
||||
paned = gtk.VPaned()
|
||||
paned.pack1(self.construct_top(), resize=False, shrink=True)
|
||||
paned.pack2(self.construct_bottom(), resize=False, shrink=True)
|
||||
self.window.add(paned)
|
||||
paned.show()
|
||||
|
||||
|
||||
def construct_top(self):
|
||||
"""Construct the top-half of the window."""
|
||||
vbox = gtk.VBox(spacing=6)
|
||||
vbox.set_border_width(12)
|
||||
vbox.show()
|
||||
|
||||
menu_bar = gtk.MenuBar()
|
||||
menu_bar.set_pack_direction(gtk.PACK_DIRECTION_RTL)
|
||||
help_menu = gtk.MenuItem("Help")
|
||||
menu = gtk.Menu()
|
||||
about_menu = gtk.MenuItem("About")
|
||||
menu.append(about_menu)
|
||||
about_menu.connect("activate", self.about_menu_response, "about")
|
||||
about_menu.show()
|
||||
help_menu.set_submenu(menu)
|
||||
help_menu.show()
|
||||
menu_bar.append(help_menu)
|
||||
vbox.pack_start(menu_bar, False, False, 2)
|
||||
menu_bar.show()
|
||||
|
||||
scrollwin = gtk.ScrolledWindow()
|
||||
scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
scrollwin.set_shadow_type(gtk.SHADOW_IN)
|
||||
vbox.pack_start(scrollwin, expand=True, fill=True)
|
||||
scrollwin.show()
|
||||
|
||||
self.treeview = gtk.TreeView()
|
||||
self.treeview.set_rules_hint(True)
|
||||
self.treeview.set_search_column(4)
|
||||
self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
|
||||
scrollwin.add(self.treeview)
|
||||
self.treeview.show()
|
||||
|
||||
cell = CellRendererGraph()
|
||||
column = gtk.TreeViewColumn()
|
||||
column.set_resizable(False)
|
||||
column.pack_start(cell, expand=False)
|
||||
column.add_attribute(cell, "node", 1)
|
||||
column.add_attribute(cell, "in-lines", 2)
|
||||
column.add_attribute(cell, "out-lines", 3)
|
||||
self.treeview.append_column(column)
|
||||
|
||||
cell = gtk.CellRendererText()
|
||||
cell.set_property("width-chars", 65)
|
||||
cell.set_property("ellipsize", pango.ELLIPSIZE_END)
|
||||
column = gtk.TreeViewColumn("Message")
|
||||
column.set_resizable(True)
|
||||
column.pack_start(cell, expand=True)
|
||||
column.add_attribute(cell, "text", 4)
|
||||
self.treeview.append_column(column)
|
||||
|
||||
cell = gtk.CellRendererText()
|
||||
cell.set_property("width-chars", 40)
|
||||
cell.set_property("ellipsize", pango.ELLIPSIZE_END)
|
||||
column = gtk.TreeViewColumn("Author")
|
||||
column.set_resizable(True)
|
||||
column.pack_start(cell, expand=True)
|
||||
column.add_attribute(cell, "text", 5)
|
||||
self.treeview.append_column(column)
|
||||
|
||||
cell = gtk.CellRendererText()
|
||||
cell.set_property("ellipsize", pango.ELLIPSIZE_END)
|
||||
column = gtk.TreeViewColumn("Date")
|
||||
column.set_resizable(True)
|
||||
column.pack_start(cell, expand=True)
|
||||
column.add_attribute(cell, "text", 6)
|
||||
self.treeview.append_column(column)
|
||||
|
||||
return vbox
|
||||
|
||||
def about_menu_response(self, widget, string):
|
||||
dialog = gtk.AboutDialog()
|
||||
dialog.set_name("Gitview")
|
||||
dialog.set_version(GitView.version)
|
||||
dialog.set_authors(["Aneesh Kumar K.V <aneesh.kumar@hp.com>"])
|
||||
dialog.set_website("http://www.kernel.org/pub/software/scm/git/")
|
||||
dialog.set_copyright("Use and distribute under the terms of the GNU General Public License")
|
||||
dialog.set_wrap_license(True)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
|
||||
def construct_bottom(self):
|
||||
"""Construct the bottom half of the window."""
|
||||
vbox = gtk.VBox(False, spacing=6)
|
||||
vbox.set_border_width(12)
|
||||
(width, height) = self.window.get_size()
|
||||
vbox.set_size_request(width, int(height / 2.5))
|
||||
vbox.show()
|
||||
|
||||
self.table = gtk.Table(rows=4, columns=4)
|
||||
self.table.set_row_spacings(6)
|
||||
self.table.set_col_spacings(6)
|
||||
vbox.pack_start(self.table, expand=False, fill=True)
|
||||
self.table.show()
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
label = gtk.Label()
|
||||
label.set_markup("<b>Revision:</b>")
|
||||
align.add(label)
|
||||
self.table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
|
||||
label.show()
|
||||
align.show()
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
self.revid_label = gtk.Label()
|
||||
self.revid_label.set_selectable(True)
|
||||
align.add(self.revid_label)
|
||||
self.table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
|
||||
self.revid_label.show()
|
||||
align.show()
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
label = gtk.Label()
|
||||
label.set_markup("<b>Committer:</b>")
|
||||
align.add(label)
|
||||
self.table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
|
||||
label.show()
|
||||
align.show()
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
self.committer_label = gtk.Label()
|
||||
self.committer_label.set_selectable(True)
|
||||
align.add(self.committer_label)
|
||||
self.table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
|
||||
self.committer_label.show()
|
||||
align.show()
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
label = gtk.Label()
|
||||
label.set_markup("<b>Timestamp:</b>")
|
||||
align.add(label)
|
||||
self.table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
|
||||
label.show()
|
||||
align.show()
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
self.timestamp_label = gtk.Label()
|
||||
self.timestamp_label.set_selectable(True)
|
||||
align.add(self.timestamp_label)
|
||||
self.table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
|
||||
self.timestamp_label.show()
|
||||
align.show()
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
label = gtk.Label()
|
||||
label.set_markup("<b>Parents:</b>")
|
||||
align.add(label)
|
||||
self.table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
|
||||
label.show()
|
||||
align.show()
|
||||
self.parents_widgets = []
|
||||
|
||||
align = gtk.Alignment(0.0, 0.5)
|
||||
label = gtk.Label()
|
||||
label.set_markup("<b>Children:</b>")
|
||||
align.add(label)
|
||||
self.table.attach(align, 2, 3, 3, 4, gtk.FILL, gtk.FILL)
|
||||
label.show()
|
||||
align.show()
|
||||
self.children_widgets = []
|
||||
|
||||
scrollwin = gtk.ScrolledWindow()
|
||||
scrollwin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
||||
scrollwin.set_shadow_type(gtk.SHADOW_IN)
|
||||
vbox.pack_start(scrollwin, expand=True, fill=True)
|
||||
scrollwin.show()
|
||||
|
||||
if have_gtksourceview:
|
||||
self.message_buffer = gtksourceview.SourceBuffer()
|
||||
slm = gtksourceview.SourceLanguagesManager()
|
||||
gsl = slm.get_language_from_mime_type("text/x-patch")
|
||||
self.message_buffer.set_highlight(True)
|
||||
self.message_buffer.set_language(gsl)
|
||||
sourceview = gtksourceview.SourceView(self.message_buffer)
|
||||
else:
|
||||
self.message_buffer = gtk.TextBuffer()
|
||||
sourceview = gtk.TextView(self.message_buffer)
|
||||
|
||||
sourceview.set_editable(False)
|
||||
sourceview.modify_font(pango.FontDescription("Monospace"))
|
||||
scrollwin.add(sourceview)
|
||||
sourceview.show()
|
||||
|
||||
return vbox
|
||||
|
||||
def _treeview_cursor_cb(self, *args):
|
||||
"""Callback for when the treeview cursor changes."""
|
||||
(path, col) = self.treeview.get_cursor()
|
||||
commit = self.model[path][0]
|
||||
|
||||
if commit.committer is not None:
|
||||
committer = commit.committer
|
||||
timestamp = commit.commit_date
|
||||
message = commit.get_message(self.with_diff)
|
||||
revid_label = commit.commit_sha1
|
||||
else:
|
||||
committer = ""
|
||||
timestamp = ""
|
||||
message = ""
|
||||
revid_label = ""
|
||||
|
||||
self.revid_label.set_text(revid_label)
|
||||
self.committer_label.set_text(committer)
|
||||
self.timestamp_label.set_text(timestamp)
|
||||
self.message_buffer.set_text(message)
|
||||
|
||||
for widget in self.parents_widgets:
|
||||
self.table.remove(widget)
|
||||
|
||||
self.parents_widgets = []
|
||||
self.table.resize(4 + len(commit.parent_sha1) - 1, 4)
|
||||
for idx, parent_id in enumerate(commit.parent_sha1):
|
||||
self.table.set_row_spacing(idx + 3, 0)
|
||||
|
||||
align = gtk.Alignment(0.0, 0.0)
|
||||
self.parents_widgets.append(align)
|
||||
self.table.attach(align, 1, 2, idx + 3, idx + 4,
|
||||
gtk.EXPAND | gtk.FILL, gtk.FILL)
|
||||
align.show()
|
||||
|
||||
hbox = gtk.HBox(False, 0)
|
||||
align.add(hbox)
|
||||
hbox.show()
|
||||
|
||||
label = gtk.Label(parent_id)
|
||||
label.set_selectable(True)
|
||||
hbox.pack_start(label, expand=False, fill=True)
|
||||
label.show()
|
||||
|
||||
image = gtk.Image()
|
||||
image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
|
||||
image.show()
|
||||
|
||||
button = gtk.Button()
|
||||
button.add(image)
|
||||
button.set_relief(gtk.RELIEF_NONE)
|
||||
button.connect("clicked", self._go_clicked_cb, parent_id)
|
||||
hbox.pack_start(button, expand=False, fill=True)
|
||||
button.show()
|
||||
|
||||
image = gtk.Image()
|
||||
image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
|
||||
image.show()
|
||||
|
||||
button = gtk.Button()
|
||||
button.add(image)
|
||||
button.set_relief(gtk.RELIEF_NONE)
|
||||
button.set_sensitive(True)
|
||||
button.connect("clicked", self._show_clicked_cb,
|
||||
commit.commit_sha1, parent_id)
|
||||
hbox.pack_start(button, expand=False, fill=True)
|
||||
button.show()
|
||||
|
||||
# Populate with child details
|
||||
for widget in self.children_widgets:
|
||||
self.table.remove(widget)
|
||||
|
||||
self.children_widgets = []
|
||||
try:
|
||||
child_sha1 = Commit.children_sha1[commit.commit_sha1]
|
||||
except KeyError:
|
||||
# We don't have child
|
||||
child_sha1 = [ 0 ]
|
||||
|
||||
if ( len(child_sha1) > len(commit.parent_sha1)):
|
||||
self.table.resize(4 + len(child_sha1) - 1, 4)
|
||||
|
||||
for idx, child_id in enumerate(child_sha1):
|
||||
self.table.set_row_spacing(idx + 3, 0)
|
||||
|
||||
align = gtk.Alignment(0.0, 0.0)
|
||||
self.children_widgets.append(align)
|
||||
self.table.attach(align, 3, 4, idx + 3, idx + 4,
|
||||
gtk.EXPAND | gtk.FILL, gtk.FILL)
|
||||
align.show()
|
||||
|
||||
hbox = gtk.HBox(False, 0)
|
||||
align.add(hbox)
|
||||
hbox.show()
|
||||
|
||||
label = gtk.Label(child_id)
|
||||
label.set_selectable(True)
|
||||
hbox.pack_start(label, expand=False, fill=True)
|
||||
label.show()
|
||||
|
||||
image = gtk.Image()
|
||||
image.set_from_stock(gtk.STOCK_JUMP_TO, gtk.ICON_SIZE_MENU)
|
||||
image.show()
|
||||
|
||||
button = gtk.Button()
|
||||
button.add(image)
|
||||
button.set_relief(gtk.RELIEF_NONE)
|
||||
button.connect("clicked", self._go_clicked_cb, child_id)
|
||||
hbox.pack_start(button, expand=False, fill=True)
|
||||
button.show()
|
||||
|
||||
image = gtk.Image()
|
||||
image.set_from_stock(gtk.STOCK_FIND, gtk.ICON_SIZE_MENU)
|
||||
image.show()
|
||||
|
||||
button = gtk.Button()
|
||||
button.add(image)
|
||||
button.set_relief(gtk.RELIEF_NONE)
|
||||
button.set_sensitive(True)
|
||||
button.connect("clicked", self._show_clicked_cb,
|
||||
child_id, commit.commit_sha1)
|
||||
hbox.pack_start(button, expand=False, fill=True)
|
||||
button.show()
|
||||
|
||||
def _destroy_cb(self, widget):
|
||||
"""Callback for when a window we manage is destroyed."""
|
||||
self.quit()
|
||||
|
||||
|
||||
def quit(self):
|
||||
"""Stop the GTK+ main loop."""
|
||||
gtk.main_quit()
|
||||
|
||||
def run(self, args):
|
||||
self.set_branch(args)
|
||||
self.window.connect("destroy", self._destroy_cb)
|
||||
self.window.show()
|
||||
gtk.main()
|
||||
|
||||
def set_branch(self, args):
|
||||
"""Fill in different windows with info from the reposiroty"""
|
||||
fp = os.popen("git rev-parse --sq --default HEAD " + list_to_string(args, 1))
|
||||
git_rev_list_cmd = fp.read()
|
||||
fp.close()
|
||||
fp = os.popen("git rev-list --header --topo-order --parents " + git_rev_list_cmd)
|
||||
self.update_window(fp)
|
||||
|
||||
def update_window(self, fp):
|
||||
commit_lines = []
|
||||
|
||||
self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
|
||||
gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str, str, str)
|
||||
|
||||
# used for cursor positioning
|
||||
self.index = {}
|
||||
|
||||
self.colours = {}
|
||||
self.nodepos = {}
|
||||
self.incomplete_line = {}
|
||||
|
||||
index = 0
|
||||
last_colour = 0
|
||||
last_nodepos = -1
|
||||
out_line = []
|
||||
input_line = fp.readline()
|
||||
while (input_line != ""):
|
||||
# The commit header ends with '\0'
|
||||
# This NULL is immediately followed by the sha1 of the
|
||||
# next commit
|
||||
if (input_line[0] != '\0'):
|
||||
commit_lines.append(input_line)
|
||||
input_line = fp.readline()
|
||||
continue;
|
||||
|
||||
commit = Commit(commit_lines)
|
||||
if (commit != None ):
|
||||
(out_line, last_colour, last_nodepos) = self.draw_graph(commit,
|
||||
index, out_line,
|
||||
last_colour,
|
||||
last_nodepos)
|
||||
self.index[commit.commit_sha1] = index
|
||||
index += 1
|
||||
|
||||
# Skip the '\0
|
||||
commit_lines = []
|
||||
commit_lines.append(input_line[1:])
|
||||
input_line = fp.readline()
|
||||
|
||||
fp.close()
|
||||
|
||||
self.treeview.set_model(self.model)
|
||||
self.treeview.show()
|
||||
|
||||
def draw_graph(self, commit, index, out_line, last_colour, last_nodepos):
|
||||
in_line=[]
|
||||
|
||||
# | -> outline
|
||||
# X
|
||||
# |\ <- inline
|
||||
|
||||
# Reset nodepostion
|
||||
if (last_nodepos > 5):
|
||||
last_nodepos = 0
|
||||
|
||||
# Add the incomplete lines of the last cell in this
|
||||
for sha1 in self.incomplete_line.keys():
|
||||
if ( sha1 != commit.commit_sha1):
|
||||
for pos in self.incomplete_line[sha1]:
|
||||
in_line.append((pos, pos, self.colours[sha1]))
|
||||
else:
|
||||
del self.incomplete_line[sha1]
|
||||
|
||||
try:
|
||||
colour = self.colours[commit.commit_sha1]
|
||||
except KeyError:
|
||||
last_colour +=1
|
||||
self.colours[commit.commit_sha1] = last_colour
|
||||
colour = last_colour
|
||||
try:
|
||||
node_pos = self.nodepos[commit.commit_sha1]
|
||||
except KeyError:
|
||||
last_nodepos +=1
|
||||
self.nodepos[commit.commit_sha1] = last_nodepos
|
||||
node_pos = last_nodepos
|
||||
|
||||
#The first parent always continue on the same line
|
||||
try:
|
||||
# check we alreay have the value
|
||||
tmp_node_pos = self.nodepos[commit.parent_sha1[0]]
|
||||
except KeyError:
|
||||
self.colours[commit.parent_sha1[0]] = colour
|
||||
self.nodepos[commit.parent_sha1[0]] = node_pos
|
||||
|
||||
in_line.append((node_pos, self.nodepos[commit.parent_sha1[0]],
|
||||
self.colours[commit.parent_sha1[0]]))
|
||||
|
||||
self.add_incomplete_line(commit.parent_sha1[0], index+1)
|
||||
|
||||
if (len(commit.parent_sha1) > 1):
|
||||
for parent_id in commit.parent_sha1[1:]:
|
||||
try:
|
||||
tmp_node_pos = self.nodepos[parent_id]
|
||||
except KeyError:
|
||||
last_colour += 1;
|
||||
self.colours[parent_id] = last_colour
|
||||
last_nodepos +=1
|
||||
self.nodepos[parent_id] = last_nodepos
|
||||
|
||||
in_line.append((node_pos, self.nodepos[parent_id],
|
||||
self.colours[parent_id]))
|
||||
self.add_incomplete_line(parent_id, index+1)
|
||||
|
||||
|
||||
try:
|
||||
branch_tag = self.bt_sha1[commit.commit_sha1]
|
||||
except KeyError:
|
||||
branch_tag = [ ]
|
||||
|
||||
|
||||
node = (node_pos, colour, branch_tag)
|
||||
|
||||
self.model.append([commit, node, out_line, in_line,
|
||||
commit.message, commit.author, commit.date])
|
||||
|
||||
return (in_line, last_colour, last_nodepos)
|
||||
|
||||
def add_incomplete_line(self, sha1, index):
|
||||
try:
|
||||
self.incomplete_line[sha1].append(self.nodepos[sha1])
|
||||
except KeyError:
|
||||
self.incomplete_line[sha1] = [self.nodepos[sha1]]
|
||||
|
||||
|
||||
def _go_clicked_cb(self, widget, revid):
|
||||
"""Callback for when the go button for a parent is clicked."""
|
||||
try:
|
||||
self.treeview.set_cursor(self.index[revid])
|
||||
except KeyError:
|
||||
print "Revision %s not present in the list" % revid
|
||||
# revid == 0 is the parent of the first commit
|
||||
if (revid != 0 ):
|
||||
print "Try running gitview without any options"
|
||||
|
||||
self.treeview.grab_focus()
|
||||
|
||||
def _show_clicked_cb(self, widget, commit_sha1, parent_sha1):
|
||||
"""Callback for when the show button for a parent is clicked."""
|
||||
window = DiffWindow()
|
||||
window.set_diff(commit_sha1, parent_sha1)
|
||||
self.treeview.grab_focus()
|
||||
|
||||
if __name__ == "__main__":
|
||||
without_diff = 0
|
||||
|
||||
if (len(sys.argv) > 1 ):
|
||||
if (sys.argv[1] == "--without-diff"):
|
||||
without_diff = 1
|
||||
|
||||
view = GitView( without_diff != 1)
|
||||
view.run(sys.argv[without_diff:])
|
||||
|
||||
|
|
@ -3,17 +3,20 @@
|
|||
# Copyright (c) 2005 Linus Torvalds
|
||||
#
|
||||
|
||||
USAGE='[-a] [-d] [-l] [-n]'
|
||||
USAGE='[-a] [-d] [-f] [-l] [-n] [-q]'
|
||||
. git-sh-setup
|
||||
|
||||
no_update_info= all_into_one= remove_redundant= local=
|
||||
no_update_info= all_into_one= remove_redundant=
|
||||
local= quiet= no_reuse_delta=
|
||||
while case "$#" in 0) break ;; esac
|
||||
do
|
||||
case "$1" in
|
||||
-n) no_update_info=t ;;
|
||||
-a) all_into_one=t ;;
|
||||
-d) remove_redundant=t ;;
|
||||
-l) local=t ;;
|
||||
-q) quiet=-q ;;
|
||||
-f) no_reuse_delta=--no-reuse-delta ;;
|
||||
-l) local=--local ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
shift
|
||||
|
@ -39,9 +42,7 @@ case ",$all_into_one," in
|
|||
find . -type f \( -name '*.pack' -o -name '*.idx' \) -print`
|
||||
;;
|
||||
esac
|
||||
if [ "$local" ]; then
|
||||
pack_objects="$pack_objects --local"
|
||||
fi
|
||||
pack_objects="$pack_objects $local $quiet $no_reuse_delta"
|
||||
name=$(git-rev-list --objects $rev_list $(git-rev-parse $rev_parse) 2>&1 |
|
||||
git-pack-objects --non-empty $pack_objects .tmp-pack) ||
|
||||
exit 1
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#ifndef NO_ICONV
|
||||
#include <iconv.h>
|
||||
#endif
|
||||
#include "git-compat-util.h"
|
||||
#include "cache.h"
|
||||
|
||||
|
@ -469,6 +471,7 @@ static int decode_b_segment(char *in, char *ot, char *ep)
|
|||
|
||||
static void convert_to_utf8(char *line, char *charset)
|
||||
{
|
||||
#ifndef NO_ICONV
|
||||
char *in, *out;
|
||||
size_t insize, outsize, nrc;
|
||||
char outbuf[4096]; /* cheat */
|
||||
|
@ -501,6 +504,7 @@ static void convert_to_utf8(char *line, char *charset)
|
|||
return;
|
||||
*out = 0;
|
||||
strcpy(line, outbuf);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void decode_header_bq(char *it)
|
||||
|
|
349
pack-objects.c
349
pack-objects.c
|
@ -5,21 +5,39 @@
|
|||
#include "csum-file.h"
|
||||
#include <sys/time.h>
|
||||
|
||||
static const char pack_usage[] = "git-pack-objects [-q] [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list";
|
||||
static const char pack_usage[] = "git-pack-objects [-q] [--no-reuse-delta] [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list";
|
||||
|
||||
struct object_entry {
|
||||
unsigned char sha1[20];
|
||||
unsigned long size;
|
||||
unsigned long offset;
|
||||
unsigned int depth;
|
||||
unsigned int hash;
|
||||
unsigned long size; /* uncompressed size */
|
||||
unsigned long offset; /* offset into the final pack file (nonzero if already written) */
|
||||
unsigned int depth; /* delta depth */
|
||||
unsigned int hash; /* name hint hash */
|
||||
enum object_type type;
|
||||
unsigned long delta_size;
|
||||
struct object_entry *delta;
|
||||
unsigned char edge; /* reused delta chain points at this entry. */
|
||||
enum object_type in_pack_type; /* could be delta */
|
||||
unsigned long delta_size; /* delta data size (uncompressed) */
|
||||
struct object_entry *delta; /* delta base object */
|
||||
struct packed_git *in_pack; /* already in pack */
|
||||
unsigned int in_pack_offset;
|
||||
};
|
||||
|
||||
/*
|
||||
* Objects we are going to pack are colected in objects array (dynamically
|
||||
* expanded). nr_objects & nr_alloc controls this array. They are stored
|
||||
* in the order we see -- typically rev-list --objects order that gives us
|
||||
* nice "minimum seek" order.
|
||||
*
|
||||
* sorted-by-sha ans sorted-by-type are arrays of pointers that point at
|
||||
* elements in the objects array. The former is used to build the pack
|
||||
* index (lists object names in the ascending order to help offset lookup),
|
||||
* and the latter is used to group similar things together by try_delta()
|
||||
* heuristics.
|
||||
*/
|
||||
|
||||
static unsigned char object_list_sha1[20];
|
||||
static int non_empty = 0;
|
||||
static int no_reuse_delta = 0;
|
||||
static int local = 0;
|
||||
static int incremental = 0;
|
||||
static struct object_entry **sorted_by_sha, **sorted_by_type;
|
||||
|
@ -29,6 +47,137 @@ static const char *base_name;
|
|||
static unsigned char pack_file_sha1[20];
|
||||
static int progress = 1;
|
||||
|
||||
/*
|
||||
* The object names in objects array are hashed with this hashtable,
|
||||
* to help looking up the entry by object name. Binary search from
|
||||
* sorted_by_sha is also possible but this was easier to code and faster.
|
||||
* This hashtable is built after all the objects are seen.
|
||||
*/
|
||||
static int *object_ix = NULL;
|
||||
static int object_ix_hashsz = 0;
|
||||
|
||||
/*
|
||||
* Pack index for existing packs give us easy access to the offsets into
|
||||
* corresponding pack file where each object's data starts, but the entries
|
||||
* do not store the size of the compressed representation (uncompressed
|
||||
* size is easily available by examining the pack entry header). We build
|
||||
* a hashtable of existing packs (pack_revindex), and keep reverse index
|
||||
* here -- pack index file is sorted by object name mapping to offset; this
|
||||
* pack_revindex[].revindex array is an ordered list of offsets, so if you
|
||||
* know the offset of an object, next offset is where its packed
|
||||
* representation ends.
|
||||
*/
|
||||
struct pack_revindex {
|
||||
struct packed_git *p;
|
||||
unsigned long *revindex;
|
||||
} *pack_revindex = NULL;
|
||||
static int pack_revindex_hashsz = 0;
|
||||
|
||||
/*
|
||||
* stats
|
||||
*/
|
||||
static int written = 0;
|
||||
static int written_delta = 0;
|
||||
static int reused = 0;
|
||||
static int reused_delta = 0;
|
||||
|
||||
static int pack_revindex_ix(struct packed_git *p)
|
||||
{
|
||||
unsigned int ui = (unsigned int) p;
|
||||
int i;
|
||||
|
||||
ui = ui ^ (ui >> 16); /* defeat structure alignment */
|
||||
i = (int)(ui % pack_revindex_hashsz);
|
||||
while (pack_revindex[i].p) {
|
||||
if (pack_revindex[i].p == p)
|
||||
return i;
|
||||
if (++i == pack_revindex_hashsz)
|
||||
i = 0;
|
||||
}
|
||||
return -1 - i;
|
||||
}
|
||||
|
||||
static void prepare_pack_ix(void)
|
||||
{
|
||||
int num;
|
||||
struct packed_git *p;
|
||||
for (num = 0, p = packed_git; p; p = p->next)
|
||||
num++;
|
||||
if (!num)
|
||||
return;
|
||||
pack_revindex_hashsz = num * 11;
|
||||
pack_revindex = xcalloc(sizeof(*pack_revindex), pack_revindex_hashsz);
|
||||
for (p = packed_git; p; p = p->next) {
|
||||
num = pack_revindex_ix(p);
|
||||
num = - 1 - num;
|
||||
pack_revindex[num].p = p;
|
||||
}
|
||||
/* revindex elements are lazily initialized */
|
||||
}
|
||||
|
||||
static int cmp_offset(const void *a_, const void *b_)
|
||||
{
|
||||
unsigned long a = *(unsigned long *) a_;
|
||||
unsigned long b = *(unsigned long *) b_;
|
||||
if (a < b)
|
||||
return -1;
|
||||
else if (a == b)
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ordered list of offsets of objects in the pack.
|
||||
*/
|
||||
static void prepare_pack_revindex(struct pack_revindex *rix)
|
||||
{
|
||||
struct packed_git *p = rix->p;
|
||||
int num_ent = num_packed_objects(p);
|
||||
int i;
|
||||
void *index = p->index_base + 256;
|
||||
|
||||
rix->revindex = xmalloc(sizeof(unsigned long) * (num_ent + 1));
|
||||
for (i = 0; i < num_ent; i++) {
|
||||
long hl = *((long *)(index + 24 * i));
|
||||
rix->revindex[i] = ntohl(hl);
|
||||
}
|
||||
/* This knows the pack format -- the 20-byte trailer
|
||||
* follows immediately after the last object data.
|
||||
*/
|
||||
rix->revindex[num_ent] = p->pack_size - 20;
|
||||
qsort(rix->revindex, num_ent, sizeof(unsigned long), cmp_offset);
|
||||
}
|
||||
|
||||
static unsigned long find_packed_object_size(struct packed_git *p,
|
||||
unsigned long ofs)
|
||||
{
|
||||
int num;
|
||||
int lo, hi;
|
||||
struct pack_revindex *rix;
|
||||
unsigned long *revindex;
|
||||
num = pack_revindex_ix(p);
|
||||
if (num < 0)
|
||||
die("internal error: pack revindex uninitialized");
|
||||
rix = &pack_revindex[num];
|
||||
if (!rix->revindex)
|
||||
prepare_pack_revindex(rix);
|
||||
revindex = rix->revindex;
|
||||
lo = 0;
|
||||
hi = num_packed_objects(p) + 1;
|
||||
do {
|
||||
int mi = (lo + hi) / 2;
|
||||
if (revindex[mi] == ofs) {
|
||||
return revindex[mi+1] - ofs;
|
||||
}
|
||||
else if (ofs < revindex[mi])
|
||||
hi = mi;
|
||||
else
|
||||
lo = mi + 1;
|
||||
} while (lo < hi);
|
||||
die("internal error: pack revindex corrupt");
|
||||
}
|
||||
|
||||
static void *delta_against(void *buf, unsigned long size, struct object_entry *entry)
|
||||
{
|
||||
unsigned long othersize, delta_size;
|
||||
|
@ -78,35 +227,69 @@ static unsigned long write_object(struct sha1file *f, struct object_entry *entry
|
|||
{
|
||||
unsigned long size;
|
||||
char type[10];
|
||||
void *buf = read_sha1_file(entry->sha1, type, &size);
|
||||
void *buf;
|
||||
unsigned char header[10];
|
||||
unsigned hdrlen, datalen;
|
||||
enum object_type obj_type;
|
||||
int to_reuse = 0;
|
||||
|
||||
obj_type = entry->type;
|
||||
if (! entry->in_pack)
|
||||
to_reuse = 0; /* can't reuse what we don't have */
|
||||
else if (obj_type == OBJ_DELTA)
|
||||
to_reuse = 1; /* check_object() decided it for us */
|
||||
else if (obj_type != entry->in_pack_type)
|
||||
to_reuse = 0; /* pack has delta which is unusable */
|
||||
else if (entry->delta)
|
||||
to_reuse = 0; /* we want to pack afresh */
|
||||
else
|
||||
to_reuse = 1; /* we have it in-pack undeltified,
|
||||
* and we do not need to deltify it.
|
||||
*/
|
||||
|
||||
if (! to_reuse) {
|
||||
buf = read_sha1_file(entry->sha1, type, &size);
|
||||
if (!buf)
|
||||
die("unable to read %s", sha1_to_hex(entry->sha1));
|
||||
if (size != entry->size)
|
||||
die("object %s size inconsistency (%lu vs %lu)", sha1_to_hex(entry->sha1), size, entry->size);
|
||||
|
||||
/*
|
||||
* The object header is a byte of 'type' followed by zero or
|
||||
* more bytes of length. For deltas, the 20 bytes of delta sha1
|
||||
* follows that.
|
||||
*/
|
||||
obj_type = entry->type;
|
||||
die("object %s size inconsistency (%lu vs %lu)",
|
||||
sha1_to_hex(entry->sha1), size, entry->size);
|
||||
if (entry->delta) {
|
||||
buf = delta_against(buf, size, entry);
|
||||
size = entry->delta_size;
|
||||
obj_type = OBJ_DELTA;
|
||||
}
|
||||
/*
|
||||
* The object header is a byte of 'type' followed by zero or
|
||||
* more bytes of length. For deltas, the 20 bytes of delta
|
||||
* sha1 follows that.
|
||||
*/
|
||||
hdrlen = encode_header(obj_type, size, header);
|
||||
sha1write(f, header, hdrlen);
|
||||
|
||||
if (entry->delta) {
|
||||
sha1write(f, entry->delta, 20);
|
||||
hdrlen += 20;
|
||||
}
|
||||
datalen = sha1write_compressed(f, buf, size);
|
||||
free(buf);
|
||||
}
|
||||
else {
|
||||
struct packed_git *p = entry->in_pack;
|
||||
use_packed_git(p);
|
||||
|
||||
datalen = find_packed_object_size(p, entry->in_pack_offset);
|
||||
buf = p->pack_base + entry->in_pack_offset;
|
||||
sha1write(f, buf, datalen);
|
||||
unuse_packed_git(p);
|
||||
hdrlen = 0; /* not really */
|
||||
if (obj_type == OBJ_DELTA)
|
||||
reused_delta++;
|
||||
reused++;
|
||||
}
|
||||
if (obj_type == OBJ_DELTA)
|
||||
written_delta++;
|
||||
written++;
|
||||
return hdrlen + datalen;
|
||||
}
|
||||
|
||||
|
@ -132,7 +315,6 @@ static void write_pack_file(void)
|
|||
int i;
|
||||
struct sha1file *f;
|
||||
unsigned long offset;
|
||||
unsigned long mb;
|
||||
struct pack_header hdr;
|
||||
|
||||
if (!base_name)
|
||||
|
@ -148,8 +330,6 @@ static void write_pack_file(void)
|
|||
offset = write_one(f, objects + i, offset);
|
||||
|
||||
sha1close(f, pack_file_sha1, 1);
|
||||
mb = offset >> 20;
|
||||
offset &= 0xfffff;
|
||||
}
|
||||
|
||||
static void write_index_file(void)
|
||||
|
@ -196,18 +376,20 @@ static int add_object_entry(unsigned char *sha1, unsigned int hash)
|
|||
{
|
||||
unsigned int idx = nr_objects;
|
||||
struct object_entry *entry;
|
||||
|
||||
if (incremental || local) {
|
||||
struct packed_git *p;
|
||||
unsigned int found_offset = 0;
|
||||
struct packed_git *found_pack = NULL;
|
||||
|
||||
for (p = packed_git; p; p = p->next) {
|
||||
struct pack_entry e;
|
||||
|
||||
if (find_pack_entry_one(sha1, &e, p)) {
|
||||
if (incremental)
|
||||
return 0;
|
||||
if (local && !p->pack_local)
|
||||
return 0;
|
||||
if (!found_pack) {
|
||||
found_offset = e.offset;
|
||||
found_pack = e.p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,15 +403,84 @@ static int add_object_entry(unsigned char *sha1, unsigned int hash)
|
|||
memset(entry, 0, sizeof(*entry));
|
||||
memcpy(entry->sha1, sha1, 20);
|
||||
entry->hash = hash;
|
||||
if (found_pack) {
|
||||
entry->in_pack = found_pack;
|
||||
entry->in_pack_offset = found_offset;
|
||||
}
|
||||
nr_objects = idx+1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int locate_object_entry_hash(unsigned char *sha1)
|
||||
{
|
||||
int i;
|
||||
unsigned int ui;
|
||||
memcpy(&ui, sha1, sizeof(unsigned int));
|
||||
i = ui % object_ix_hashsz;
|
||||
while (0 < object_ix[i]) {
|
||||
if (!memcmp(sha1, objects[object_ix[i]-1].sha1, 20))
|
||||
return i;
|
||||
if (++i == object_ix_hashsz)
|
||||
i = 0;
|
||||
}
|
||||
return -1 - i;
|
||||
}
|
||||
|
||||
static struct object_entry *locate_object_entry(unsigned char *sha1)
|
||||
{
|
||||
int i = locate_object_entry_hash(sha1);
|
||||
if (0 <= i)
|
||||
return &objects[object_ix[i]-1];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void check_object(struct object_entry *entry)
|
||||
{
|
||||
char type[20];
|
||||
|
||||
if (!sha1_object_info(entry->sha1, type, &entry->size)) {
|
||||
if (entry->in_pack) {
|
||||
unsigned char base[20];
|
||||
unsigned long size;
|
||||
struct object_entry *base_entry;
|
||||
|
||||
/* We want in_pack_type even if we do not reuse delta.
|
||||
* There is no point not reusing non-delta representations.
|
||||
*/
|
||||
check_reuse_pack_delta(entry->in_pack,
|
||||
entry->in_pack_offset,
|
||||
base, &size,
|
||||
&entry->in_pack_type);
|
||||
|
||||
/* Check if it is delta, and the base is also an object
|
||||
* we are going to pack. If so we will reuse the existing
|
||||
* delta.
|
||||
*/
|
||||
if (!no_reuse_delta &&
|
||||
entry->in_pack_type == OBJ_DELTA &&
|
||||
(base_entry = locate_object_entry(base))) {
|
||||
|
||||
/* Depth value does not matter - find_deltas()
|
||||
* will never consider reused delta as the
|
||||
* base object to deltify other objects
|
||||
* against, in order to avoid circular deltas.
|
||||
*/
|
||||
|
||||
/* uncompressed size of the delta data */
|
||||
entry->size = entry->delta_size = size;
|
||||
entry->delta = base_entry;
|
||||
entry->type = OBJ_DELTA;
|
||||
|
||||
base_entry->edge = 1;
|
||||
|
||||
return;
|
||||
}
|
||||
/* Otherwise we would do the usual */
|
||||
}
|
||||
|
||||
if (sha1_object_info(entry->sha1, type, &entry->size))
|
||||
die("unable to get type of object %s",
|
||||
sha1_to_hex(entry->sha1));
|
||||
|
||||
if (!strcmp(type, "commit")) {
|
||||
entry->type = OBJ_COMMIT;
|
||||
} else if (!strcmp(type, "tree")) {
|
||||
|
@ -241,10 +492,25 @@ static void check_object(struct object_entry *entry)
|
|||
} else
|
||||
die("unable to pack object %s of type %s",
|
||||
sha1_to_hex(entry->sha1), type);
|
||||
}
|
||||
|
||||
static void hash_objects(void)
|
||||
{
|
||||
int i;
|
||||
struct object_entry *oe;
|
||||
|
||||
object_ix_hashsz = nr_objects * 2;
|
||||
object_ix = xcalloc(sizeof(int), object_ix_hashsz);
|
||||
for (i = 0, oe = objects; i < nr_objects; i++, oe++) {
|
||||
int ix = locate_object_entry_hash(oe->sha1);
|
||||
if (0 <= ix) {
|
||||
error("the same object '%s' added twice",
|
||||
sha1_to_hex(oe->sha1));
|
||||
continue;
|
||||
}
|
||||
ix = -1 - ix;
|
||||
object_ix[ix] = i + 1;
|
||||
}
|
||||
else
|
||||
die("unable to get type of object %s",
|
||||
sha1_to_hex(entry->sha1));
|
||||
}
|
||||
|
||||
static void get_object_details(void)
|
||||
|
@ -252,6 +518,8 @@ static void get_object_details(void)
|
|||
int i;
|
||||
struct object_entry *entry = objects;
|
||||
|
||||
hash_objects();
|
||||
prepare_pack_ix();
|
||||
for (i = 0; i < nr_objects; i++)
|
||||
check_object(entry++);
|
||||
}
|
||||
|
@ -326,6 +594,13 @@ static int try_delta(struct unpacked *cur, struct unpacked *old, unsigned max_de
|
|||
if (cur_entry->type != old_entry->type)
|
||||
return -1;
|
||||
|
||||
/* If the current object is at edge, take the depth the objects
|
||||
* that depend on the current object into account -- otherwise
|
||||
* they would become too deep.
|
||||
*/
|
||||
if (cur_entry->edge)
|
||||
max_depth /= 4;
|
||||
|
||||
size = cur_entry->size;
|
||||
if (size < 50)
|
||||
return -1;
|
||||
|
@ -382,11 +657,19 @@ static void find_deltas(struct object_entry **list, int window, int depth)
|
|||
eye_candy -= nr_objects / 20;
|
||||
fputc('.', stderr);
|
||||
}
|
||||
|
||||
if (entry->delta)
|
||||
/* This happens if we decided to reuse existing
|
||||
* delta from a pack. "!no_reuse_delta &&" is implied.
|
||||
*/
|
||||
continue;
|
||||
|
||||
free(n->data);
|
||||
n->entry = entry;
|
||||
n->data = read_sha1_file(entry->sha1, type, &size);
|
||||
if (size != entry->size)
|
||||
die("object %s inconsistent object length (%lu vs %lu)", sha1_to_hex(entry->sha1), size, entry->size);
|
||||
|
||||
j = window;
|
||||
while (--j > 0) {
|
||||
unsigned int other_idx = idx + j;
|
||||
|
@ -411,10 +694,12 @@ static void find_deltas(struct object_entry **list, int window, int depth)
|
|||
|
||||
static void prepare_pack(int window, int depth)
|
||||
{
|
||||
get_object_details();
|
||||
|
||||
if (progress)
|
||||
fprintf(stderr, "Packing %d objects", nr_objects);
|
||||
get_object_details();
|
||||
if (progress)
|
||||
fputc('.', stderr);
|
||||
|
||||
sorted_by_type = create_sorted_list(type_size_sort);
|
||||
if (window && depth)
|
||||
find_deltas(sorted_by_type, window+1, depth);
|
||||
|
@ -443,6 +728,7 @@ static int reuse_cached_pack(unsigned char *sha1, int pack_to_stdout)
|
|||
}
|
||||
}
|
||||
|
||||
if (progress)
|
||||
fprintf(stderr, "Reusing %d objects pack %s\n", nr_objects,
|
||||
sha1_to_hex(sha1));
|
||||
|
||||
|
@ -524,6 +810,10 @@ int main(int argc, char **argv)
|
|||
progress = 0;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp("--no-reuse-delta", arg)) {
|
||||
no_reuse_delta = 1;
|
||||
continue;
|
||||
}
|
||||
if (!strcmp("--stdout", arg)) {
|
||||
pack_to_stdout = 1;
|
||||
continue;
|
||||
|
@ -599,5 +889,8 @@ int main(int argc, char **argv)
|
|||
puts(sha1_to_hex(object_list_sha1));
|
||||
}
|
||||
}
|
||||
if (progress)
|
||||
fprintf(stderr, "Total %d, written %d (delta %d), reused %d (delta %d)\n",
|
||||
nr_objects, written, written_delta, reused, reused_delta);
|
||||
return 0;
|
||||
}
|
||||
|
|
4
pack.h
4
pack.h
|
@ -29,5 +29,7 @@ struct pack_header {
|
|||
};
|
||||
|
||||
extern int verify_pack(struct packed_git *, int);
|
||||
|
||||
extern int check_reuse_pack_delta(struct packed_git *, unsigned long,
|
||||
unsigned char *, unsigned long *,
|
||||
enum object_type *);
|
||||
#endif
|
||||
|
|
19
sha1_file.c
19
sha1_file.c
|
@ -826,6 +826,25 @@ static unsigned long unpack_object_header(struct packed_git *p, unsigned long of
|
|||
return offset;
|
||||
}
|
||||
|
||||
int check_reuse_pack_delta(struct packed_git *p, unsigned long offset,
|
||||
unsigned char *base, unsigned long *sizep,
|
||||
enum object_type *kindp)
|
||||
{
|
||||
unsigned long ptr;
|
||||
int status = -1;
|
||||
|
||||
use_packed_git(p);
|
||||
ptr = offset;
|
||||
ptr = unpack_object_header(p, ptr, kindp, sizep);
|
||||
if (*kindp != OBJ_DELTA)
|
||||
goto done;
|
||||
memcpy(base, p->pack_base + ptr, 20);
|
||||
status = 0;
|
||||
done:
|
||||
unuse_packed_git(p);
|
||||
return status;
|
||||
}
|
||||
|
||||
void packed_object_info_detail(struct pack_entry *e,
|
||||
char *type,
|
||||
unsigned long *size,
|
||||
|
|
Loading…
Reference in a new issue