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
|
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
|
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)
|
any of the directories on $GIT_ALTERNATE_OBJECT_DIRECTORIES)
|
||||||
enables git to read from such an archive.
|
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
|
OPTIONS
|
||||||
-------
|
-------
|
||||||
|
@ -74,6 +81,18 @@ base-name::
|
||||||
Only create a packed archive if it would contain at
|
Only create a packed archive if it would contain at
|
||||||
least one object.
|
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
|
Author
|
||||||
------
|
------
|
||||||
Written by Linus Torvalds <torvalds@osdl.org>
|
Written by Linus Torvalds <torvalds@osdl.org>
|
||||||
|
|
|
@ -9,7 +9,7 @@ objects into pack files.
|
||||||
|
|
||||||
SYNOPSIS
|
SYNOPSIS
|
||||||
--------
|
--------
|
||||||
'git-repack' [-a] [-d] [-l] [-n]
|
'git-repack' [-a] [-d] [-f] [-l] [-n] [-q]
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
-----------
|
-----------
|
||||||
|
@ -43,6 +43,14 @@ OPTIONS
|
||||||
Pass the `--local` option to `git pack-objects`, see
|
Pass the `--local` option to `git pack-objects`, see
|
||||||
gitlink:git-pack-objects[1].
|
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::
|
-n::
|
||||||
Do not update the server information with
|
Do not update the server information with
|
||||||
`git update-server-info`.
|
`git update-server-info`.
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -53,6 +53,8 @@ all:
|
||||||
# Define NO_SOCKADDR_STORAGE if your platform does not have struct
|
# Define NO_SOCKADDR_STORAGE if your platform does not have struct
|
||||||
# sockaddr_storage.
|
# sockaddr_storage.
|
||||||
#
|
#
|
||||||
|
# Define NO_ICONV if your libc does not properly support iconv.
|
||||||
|
#
|
||||||
# Define COLLISION_CHECK below if you believe that SHA1's
|
# Define COLLISION_CHECK below if you believe that SHA1's
|
||||||
# 1461501637330902918203684832716283019655932542976 hashes do not give you
|
# 1461501637330902918203684832716283019655932542976 hashes do not give you
|
||||||
# sufficient guarantee that no collisions between objects will ever happen.
|
# sufficient guarantee that no collisions between objects will ever happen.
|
||||||
|
@ -380,6 +382,10 @@ else
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifdef NO_ICONV
|
||||||
|
ALL_CFLAGS += -DNO_ICONV
|
||||||
|
endif
|
||||||
|
|
||||||
ifdef PPC_SHA1
|
ifdef PPC_SHA1
|
||||||
SHA1_HEADER = "ppc/sha1.h"
|
SHA1_HEADER = "ppc/sha1.h"
|
||||||
LIB_OBJS += ppc/sha1.o ppc/sha1ppc.o
|
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
|
# Copyright (c) 2005 Linus Torvalds
|
||||||
#
|
#
|
||||||
|
|
||||||
USAGE='[-a] [-d] [-l] [-n]'
|
USAGE='[-a] [-d] [-f] [-l] [-n] [-q]'
|
||||||
. git-sh-setup
|
. 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
|
while case "$#" in 0) break ;; esac
|
||||||
do
|
do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-n) no_update_info=t ;;
|
-n) no_update_info=t ;;
|
||||||
-a) all_into_one=t ;;
|
-a) all_into_one=t ;;
|
||||||
-d) remove_redundant=t ;;
|
-d) remove_redundant=t ;;
|
||||||
-l) local=t ;;
|
-q) quiet=-q ;;
|
||||||
|
-f) no_reuse_delta=--no-reuse-delta ;;
|
||||||
|
-l) local=--local ;;
|
||||||
*) usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
shift
|
shift
|
||||||
|
@ -39,9 +42,7 @@ case ",$all_into_one," in
|
||||||
find . -type f \( -name '*.pack' -o -name '*.idx' \) -print`
|
find . -type f \( -name '*.pack' -o -name '*.idx' \) -print`
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
if [ "$local" ]; then
|
pack_objects="$pack_objects $local $quiet $no_reuse_delta"
|
||||||
pack_objects="$pack_objects --local"
|
|
||||||
fi
|
|
||||||
name=$(git-rev-list --objects $rev_list $(git-rev-parse $rev_parse) 2>&1 |
|
name=$(git-rev-list --objects $rev_list $(git-rev-parse $rev_parse) 2>&1 |
|
||||||
git-pack-objects --non-empty $pack_objects .tmp-pack) ||
|
git-pack-objects --non-empty $pack_objects .tmp-pack) ||
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
#ifndef NO_ICONV
|
||||||
#include <iconv.h>
|
#include <iconv.h>
|
||||||
|
#endif
|
||||||
#include "git-compat-util.h"
|
#include "git-compat-util.h"
|
||||||
#include "cache.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)
|
static void convert_to_utf8(char *line, char *charset)
|
||||||
{
|
{
|
||||||
|
#ifndef NO_ICONV
|
||||||
char *in, *out;
|
char *in, *out;
|
||||||
size_t insize, outsize, nrc;
|
size_t insize, outsize, nrc;
|
||||||
char outbuf[4096]; /* cheat */
|
char outbuf[4096]; /* cheat */
|
||||||
|
@ -501,6 +504,7 @@ static void convert_to_utf8(char *line, char *charset)
|
||||||
return;
|
return;
|
||||||
*out = 0;
|
*out = 0;
|
||||||
strcpy(line, outbuf);
|
strcpy(line, outbuf);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static void decode_header_bq(char *it)
|
static void decode_header_bq(char *it)
|
||||||
|
|
413
pack-objects.c
413
pack-objects.c
|
@ -5,21 +5,39 @@
|
||||||
#include "csum-file.h"
|
#include "csum-file.h"
|
||||||
#include <sys/time.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 {
|
struct object_entry {
|
||||||
unsigned char sha1[20];
|
unsigned char sha1[20];
|
||||||
unsigned long size;
|
unsigned long size; /* uncompressed size */
|
||||||
unsigned long offset;
|
unsigned long offset; /* offset into the final pack file (nonzero if already written) */
|
||||||
unsigned int depth;
|
unsigned int depth; /* delta depth */
|
||||||
unsigned int hash;
|
unsigned int hash; /* name hint hash */
|
||||||
enum object_type type;
|
enum object_type type;
|
||||||
unsigned long delta_size;
|
unsigned char edge; /* reused delta chain points at this entry. */
|
||||||
struct object_entry *delta;
|
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 unsigned char object_list_sha1[20];
|
||||||
static int non_empty = 0;
|
static int non_empty = 0;
|
||||||
|
static int no_reuse_delta = 0;
|
||||||
static int local = 0;
|
static int local = 0;
|
||||||
static int incremental = 0;
|
static int incremental = 0;
|
||||||
static struct object_entry **sorted_by_sha, **sorted_by_type;
|
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 unsigned char pack_file_sha1[20];
|
||||||
static int progress = 1;
|
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)
|
static void *delta_against(void *buf, unsigned long size, struct object_entry *entry)
|
||||||
{
|
{
|
||||||
unsigned long othersize, delta_size;
|
unsigned long othersize, delta_size;
|
||||||
|
@ -78,35 +227,69 @@ static unsigned long write_object(struct sha1file *f, struct object_entry *entry
|
||||||
{
|
{
|
||||||
unsigned long size;
|
unsigned long size;
|
||||||
char type[10];
|
char type[10];
|
||||||
void *buf = read_sha1_file(entry->sha1, type, &size);
|
void *buf;
|
||||||
unsigned char header[10];
|
unsigned char header[10];
|
||||||
unsigned hdrlen, datalen;
|
unsigned hdrlen, datalen;
|
||||||
enum object_type obj_type;
|
enum object_type obj_type;
|
||||||
|
int to_reuse = 0;
|
||||||
|
|
||||||
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;
|
obj_type = entry->type;
|
||||||
if (entry->delta) {
|
if (! entry->in_pack)
|
||||||
buf = delta_against(buf, size, entry);
|
to_reuse = 0; /* can't reuse what we don't have */
|
||||||
size = entry->delta_size;
|
else if (obj_type == OBJ_DELTA)
|
||||||
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);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
hdrlen = encode_header(obj_type, size, header);
|
else {
|
||||||
sha1write(f, header, hdrlen);
|
struct packed_git *p = entry->in_pack;
|
||||||
if (entry->delta) {
|
use_packed_git(p);
|
||||||
sha1write(f, entry->delta, 20);
|
|
||||||
hdrlen += 20;
|
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++;
|
||||||
}
|
}
|
||||||
datalen = sha1write_compressed(f, buf, size);
|
if (obj_type == OBJ_DELTA)
|
||||||
free(buf);
|
written_delta++;
|
||||||
|
written++;
|
||||||
return hdrlen + datalen;
|
return hdrlen + datalen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +315,6 @@ static void write_pack_file(void)
|
||||||
int i;
|
int i;
|
||||||
struct sha1file *f;
|
struct sha1file *f;
|
||||||
unsigned long offset;
|
unsigned long offset;
|
||||||
unsigned long mb;
|
|
||||||
struct pack_header hdr;
|
struct pack_header hdr;
|
||||||
|
|
||||||
if (!base_name)
|
if (!base_name)
|
||||||
|
@ -148,8 +330,6 @@ static void write_pack_file(void)
|
||||||
offset = write_one(f, objects + i, offset);
|
offset = write_one(f, objects + i, offset);
|
||||||
|
|
||||||
sha1close(f, pack_file_sha1, 1);
|
sha1close(f, pack_file_sha1, 1);
|
||||||
mb = offset >> 20;
|
|
||||||
offset &= 0xfffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void write_index_file(void)
|
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;
|
unsigned int idx = nr_objects;
|
||||||
struct object_entry *entry;
|
struct object_entry *entry;
|
||||||
|
struct packed_git *p;
|
||||||
|
unsigned int found_offset = 0;
|
||||||
|
struct packed_git *found_pack = NULL;
|
||||||
|
|
||||||
if (incremental || local) {
|
for (p = packed_git; p; p = p->next) {
|
||||||
struct packed_git *p;
|
struct pack_entry e;
|
||||||
|
if (find_pack_entry_one(sha1, &e, p)) {
|
||||||
for (p = packed_git; p; p = p->next) {
|
if (incremental)
|
||||||
struct pack_entry e;
|
return 0;
|
||||||
|
if (local && !p->pack_local)
|
||||||
if (find_pack_entry_one(sha1, &e, p)) {
|
return 0;
|
||||||
if (incremental)
|
if (!found_pack) {
|
||||||
return 0;
|
found_offset = e.offset;
|
||||||
if (local && !p->pack_local)
|
found_pack = e.p;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,30 +403,114 @@ static int add_object_entry(unsigned char *sha1, unsigned int hash)
|
||||||
memset(entry, 0, sizeof(*entry));
|
memset(entry, 0, sizeof(*entry));
|
||||||
memcpy(entry->sha1, sha1, 20);
|
memcpy(entry->sha1, sha1, 20);
|
||||||
entry->hash = hash;
|
entry->hash = hash;
|
||||||
|
if (found_pack) {
|
||||||
|
entry->in_pack = found_pack;
|
||||||
|
entry->in_pack_offset = found_offset;
|
||||||
|
}
|
||||||
nr_objects = idx+1;
|
nr_objects = idx+1;
|
||||||
return 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)
|
static void check_object(struct object_entry *entry)
|
||||||
{
|
{
|
||||||
char type[20];
|
char type[20];
|
||||||
|
|
||||||
if (!sha1_object_info(entry->sha1, type, &entry->size)) {
|
if (entry->in_pack) {
|
||||||
if (!strcmp(type, "commit")) {
|
unsigned char base[20];
|
||||||
entry->type = OBJ_COMMIT;
|
unsigned long size;
|
||||||
} else if (!strcmp(type, "tree")) {
|
struct object_entry *base_entry;
|
||||||
entry->type = OBJ_TREE;
|
|
||||||
} else if (!strcmp(type, "blob")) {
|
/* We want in_pack_type even if we do not reuse delta.
|
||||||
entry->type = OBJ_BLOB;
|
* There is no point not reusing non-delta representations.
|
||||||
} else if (!strcmp(type, "tag")) {
|
*/
|
||||||
entry->type = OBJ_TAG;
|
check_reuse_pack_delta(entry->in_pack,
|
||||||
} else
|
entry->in_pack_offset,
|
||||||
die("unable to pack object %s of type %s",
|
base, &size,
|
||||||
sha1_to_hex(entry->sha1), type);
|
&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 */
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (sha1_object_info(entry->sha1, type, &entry->size))
|
||||||
die("unable to get type of object %s",
|
die("unable to get type of object %s",
|
||||||
sha1_to_hex(entry->sha1));
|
sha1_to_hex(entry->sha1));
|
||||||
|
|
||||||
|
if (!strcmp(type, "commit")) {
|
||||||
|
entry->type = OBJ_COMMIT;
|
||||||
|
} else if (!strcmp(type, "tree")) {
|
||||||
|
entry->type = OBJ_TREE;
|
||||||
|
} else if (!strcmp(type, "blob")) {
|
||||||
|
entry->type = OBJ_BLOB;
|
||||||
|
} else if (!strcmp(type, "tag")) {
|
||||||
|
entry->type = OBJ_TAG;
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void get_object_details(void)
|
static void get_object_details(void)
|
||||||
|
@ -252,6 +518,8 @@ static void get_object_details(void)
|
||||||
int i;
|
int i;
|
||||||
struct object_entry *entry = objects;
|
struct object_entry *entry = objects;
|
||||||
|
|
||||||
|
hash_objects();
|
||||||
|
prepare_pack_ix();
|
||||||
for (i = 0; i < nr_objects; i++)
|
for (i = 0; i < nr_objects; i++)
|
||||||
check_object(entry++);
|
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)
|
if (cur_entry->type != old_entry->type)
|
||||||
return -1;
|
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;
|
size = cur_entry->size;
|
||||||
if (size < 50)
|
if (size < 50)
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -382,11 +657,19 @@ static void find_deltas(struct object_entry **list, int window, int depth)
|
||||||
eye_candy -= nr_objects / 20;
|
eye_candy -= nr_objects / 20;
|
||||||
fputc('.', stderr);
|
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);
|
free(n->data);
|
||||||
n->entry = entry;
|
n->entry = entry;
|
||||||
n->data = read_sha1_file(entry->sha1, type, &size);
|
n->data = read_sha1_file(entry->sha1, type, &size);
|
||||||
if (size != entry->size)
|
if (size != entry->size)
|
||||||
die("object %s inconsistent object length (%lu vs %lu)", sha1_to_hex(entry->sha1), size, entry->size);
|
die("object %s inconsistent object length (%lu vs %lu)", sha1_to_hex(entry->sha1), size, entry->size);
|
||||||
|
|
||||||
j = window;
|
j = window;
|
||||||
while (--j > 0) {
|
while (--j > 0) {
|
||||||
unsigned int other_idx = idx + j;
|
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)
|
static void prepare_pack(int window, int depth)
|
||||||
{
|
{
|
||||||
get_object_details();
|
|
||||||
|
|
||||||
if (progress)
|
if (progress)
|
||||||
fprintf(stderr, "Packing %d objects", nr_objects);
|
fprintf(stderr, "Packing %d objects", nr_objects);
|
||||||
|
get_object_details();
|
||||||
|
if (progress)
|
||||||
|
fputc('.', stderr);
|
||||||
|
|
||||||
sorted_by_type = create_sorted_list(type_size_sort);
|
sorted_by_type = create_sorted_list(type_size_sort);
|
||||||
if (window && depth)
|
if (window && depth)
|
||||||
find_deltas(sorted_by_type, window+1, depth);
|
find_deltas(sorted_by_type, window+1, depth);
|
||||||
|
@ -443,8 +728,9 @@ static int reuse_cached_pack(unsigned char *sha1, int pack_to_stdout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(stderr, "Reusing %d objects pack %s\n", nr_objects,
|
if (progress)
|
||||||
sha1_to_hex(sha1));
|
fprintf(stderr, "Reusing %d objects pack %s\n", nr_objects,
|
||||||
|
sha1_to_hex(sha1));
|
||||||
|
|
||||||
if (pack_to_stdout) {
|
if (pack_to_stdout) {
|
||||||
if (copy_fd(ifd, 1))
|
if (copy_fd(ifd, 1))
|
||||||
|
@ -524,6 +810,10 @@ int main(int argc, char **argv)
|
||||||
progress = 0;
|
progress = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!strcmp("--no-reuse-delta", arg)) {
|
||||||
|
no_reuse_delta = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!strcmp("--stdout", arg)) {
|
if (!strcmp("--stdout", arg)) {
|
||||||
pack_to_stdout = 1;
|
pack_to_stdout = 1;
|
||||||
continue;
|
continue;
|
||||||
|
@ -599,5 +889,8 @@ int main(int argc, char **argv)
|
||||||
puts(sha1_to_hex(object_list_sha1));
|
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;
|
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 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
|
#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;
|
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,
|
void packed_object_info_detail(struct pack_entry *e,
|
||||||
char *type,
|
char *type,
|
||||||
unsigned long *size,
|
unsigned long *size,
|
||||||
|
|
Loading…
Reference in a new issue