diff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt index 2d67d3911e..4cb2e83faa 100644 --- a/Documentation/git-pack-objects.txt +++ b/Documentation/git-pack-objects.txt @@ -8,7 +8,10 @@ git-pack-objects - Create a packed archive of objects. SYNOPSIS -------- -'git-pack-objects' [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list +[verse] +'git-pack-objects' [-q] [--no-reuse-delta] [--non-empty] + [--local] [--incremental] [--window=N] [--depth=N] + {--stdout | base-name} < object-list DESCRIPTION @@ -32,6 +35,10 @@ Placing both in the pack/ subdirectory of $GIT_OBJECT_DIRECTORY (or any of the directories on $GIT_ALTERNATE_OBJECT_DIRECTORIES) enables git to read from such an archive. +In a packed archive, an object is either stored as a compressed +whole, or as a difference from some other object. The latter is +often called a delta. + OPTIONS ------- @@ -74,6 +81,18 @@ base-name:: Only create a packed archive if it would contain at least one object. +-q:: + This flag makes the command not to report its progress + on the standard error stream. + +--no-reuse-delta:: + When creating a packed archive in a repository that + has existing packs, the command reuses existing deltas. + This sometimes results in a slightly suboptimal pack. + This flag tells the command not to reuse existing deltas + but compute them from scratch. + + Author ------ Written by Linus Torvalds diff --git a/Documentation/git-repack.txt b/Documentation/git-repack.txt index 9060fe8897..6c0f792dad 100644 --- a/Documentation/git-repack.txt +++ b/Documentation/git-repack.txt @@ -9,7 +9,7 @@ objects into pack files. SYNOPSIS -------- -'git-repack' [-a] [-d] [-l] [-n] +'git-repack' [-a] [-d] [-f] [-l] [-n] [-q] DESCRIPTION ----------- @@ -43,6 +43,14 @@ OPTIONS Pass the `--local` option to `git pack-objects`, see gitlink:git-pack-objects[1]. +-f:: + Pass the `--no-reuse-delta` option to `git pack-objects`, see + gitlink:git-pack-objects[1]. + +-q:: + Pass the `-q` option to `git pack-objects`, see + gitlink:git-pack-objects[1]. + -n:: Do not update the server information with `git update-server-info`. diff --git a/Makefile b/Makefile index 80d7f28ce1..ccb896e812 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,8 @@ all: # Define NO_SOCKADDR_STORAGE if your platform does not have struct # sockaddr_storage. # +# Define NO_ICONV if your libc does not properly support iconv. +# # Define COLLISION_CHECK below if you believe that SHA1's # 1461501637330902918203684832716283019655932542976 hashes do not give you # sufficient guarantee that no collisions between objects will ever happen. @@ -380,6 +382,10 @@ else endif endif +ifdef NO_ICONV + ALL_CFLAGS += -DNO_ICONV +endif + ifdef PPC_SHA1 SHA1_HEADER = "ppc/sha1.h" LIB_OBJS += ppc/sha1.o ppc/sha1ppc.o diff --git a/contrib/git-svn/git-svn b/contrib/git-svn/git-svn new file mode 100755 index 0000000000..2caf0570ff --- /dev/null +++ b/contrib/git-svn/git-svn @@ -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 '; +$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 [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 () { + 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 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) +} +; diff --git a/contrib/git-svn/git-svn.txt b/contrib/git-svn/git-svn.txt new file mode 100644 index 0000000000..4b79fb0be6 --- /dev/null +++ b/contrib/git-svn/git-svn.txt @@ -0,0 +1,208 @@ +git-svn(1) +========== + +NAME +---- +git-svn - bidirectional operation between a single Subversion branch and git + +SYNOPSIS +-------- +'git-svn' [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 :: +--revision :: + Only used with the 'fetch' command. + + Takes any valid -r svn would accept and passes it + directly to svn. -r: 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 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 [ ...] +# 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 . + +Documentation +------------- +Written by Eric Wong . diff --git a/contrib/gitview/gitview b/contrib/gitview/gitview new file mode 100755 index 0000000000..5862fcca41 --- /dev/null +++ b/contrib/gitview/gitview @@ -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 +""" +__copyright__ = "Copyright (C) 2006 Hewlett-Packard Development Company, L.P." +__author__ = "Aneesh Kumar K.V " + + +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.*) (?P\d+) (?P[+-]\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 "]) + 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("Revision:") + 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("Committer:") + 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("Timestamp:") + 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("Parents:") + 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("Children:") + 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:]) + + diff --git a/git-repack.sh b/git-repack.sh index 1fafb6ecf6..3d6fec1c9a 100755 --- a/git-repack.sh +++ b/git-repack.sh @@ -3,17 +3,20 @@ # Copyright (c) 2005 Linus Torvalds # -USAGE='[-a] [-d] [-l] [-n]' +USAGE='[-a] [-d] [-f] [-l] [-n] [-q]' . git-sh-setup -no_update_info= all_into_one= remove_redundant= local= +no_update_info= all_into_one= remove_redundant= +local= quiet= no_reuse_delta= while case "$#" in 0) break ;; esac do case "$1" in -n) no_update_info=t ;; -a) all_into_one=t ;; -d) remove_redundant=t ;; - -l) local=t ;; + -q) quiet=-q ;; + -f) no_reuse_delta=--no-reuse-delta ;; + -l) local=--local ;; *) usage ;; esac shift @@ -39,9 +42,7 @@ case ",$all_into_one," in find . -type f \( -name '*.pack' -o -name '*.idx' \) -print` ;; esac -if [ "$local" ]; then - pack_objects="$pack_objects --local" -fi +pack_objects="$pack_objects $local $quiet $no_reuse_delta" name=$(git-rev-list --objects $rev_list $(git-rev-parse $rev_parse) 2>&1 | git-pack-objects --non-empty $pack_objects .tmp-pack) || exit 1 diff --git a/mailinfo.c b/mailinfo.c index ff2d4d4038..3c56f8c108 100644 --- a/mailinfo.c +++ b/mailinfo.c @@ -7,7 +7,9 @@ #include #include #include +#ifndef NO_ICONV #include +#endif #include "git-compat-util.h" #include "cache.h" @@ -469,6 +471,7 @@ static int decode_b_segment(char *in, char *ot, char *ep) static void convert_to_utf8(char *line, char *charset) { +#ifndef NO_ICONV char *in, *out; size_t insize, outsize, nrc; char outbuf[4096]; /* cheat */ @@ -501,6 +504,7 @@ static void convert_to_utf8(char *line, char *charset) return; *out = 0; strcpy(line, outbuf); +#endif } static void decode_header_bq(char *it) diff --git a/pack-objects.c b/pack-objects.c index c5a5e61605..38e1c9921b 100644 --- a/pack-objects.c +++ b/pack-objects.c @@ -5,21 +5,39 @@ #include "csum-file.h" #include -static const char pack_usage[] = "git-pack-objects [-q] [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list"; +static const char pack_usage[] = "git-pack-objects [-q] [--no-reuse-delta] [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] {--stdout | base-name} < object-list"; struct object_entry { unsigned char sha1[20]; - unsigned long size; - unsigned long offset; - unsigned int depth; - unsigned int hash; + unsigned long size; /* uncompressed size */ + unsigned long offset; /* offset into the final pack file (nonzero if already written) */ + unsigned int depth; /* delta depth */ + unsigned int hash; /* name hint hash */ enum object_type type; - unsigned long delta_size; - struct object_entry *delta; + unsigned char edge; /* reused delta chain points at this entry. */ + enum object_type in_pack_type; /* could be delta */ + unsigned long delta_size; /* delta data size (uncompressed) */ + struct object_entry *delta; /* delta base object */ + struct packed_git *in_pack; /* already in pack */ + unsigned int in_pack_offset; }; +/* + * Objects we are going to pack are colected in objects array (dynamically + * expanded). nr_objects & nr_alloc controls this array. They are stored + * in the order we see -- typically rev-list --objects order that gives us + * nice "minimum seek" order. + * + * sorted-by-sha ans sorted-by-type are arrays of pointers that point at + * elements in the objects array. The former is used to build the pack + * index (lists object names in the ascending order to help offset lookup), + * and the latter is used to group similar things together by try_delta() + * heuristics. + */ + static unsigned char object_list_sha1[20]; static int non_empty = 0; +static int no_reuse_delta = 0; static int local = 0; static int incremental = 0; static struct object_entry **sorted_by_sha, **sorted_by_type; @@ -29,6 +47,137 @@ static const char *base_name; static unsigned char pack_file_sha1[20]; static int progress = 1; +/* + * The object names in objects array are hashed with this hashtable, + * to help looking up the entry by object name. Binary search from + * sorted_by_sha is also possible but this was easier to code and faster. + * This hashtable is built after all the objects are seen. + */ +static int *object_ix = NULL; +static int object_ix_hashsz = 0; + +/* + * Pack index for existing packs give us easy access to the offsets into + * corresponding pack file where each object's data starts, but the entries + * do not store the size of the compressed representation (uncompressed + * size is easily available by examining the pack entry header). We build + * a hashtable of existing packs (pack_revindex), and keep reverse index + * here -- pack index file is sorted by object name mapping to offset; this + * pack_revindex[].revindex array is an ordered list of offsets, so if you + * know the offset of an object, next offset is where its packed + * representation ends. + */ +struct pack_revindex { + struct packed_git *p; + unsigned long *revindex; +} *pack_revindex = NULL; +static int pack_revindex_hashsz = 0; + +/* + * stats + */ +static int written = 0; +static int written_delta = 0; +static int reused = 0; +static int reused_delta = 0; + +static int pack_revindex_ix(struct packed_git *p) +{ + unsigned int ui = (unsigned int) p; + int i; + + ui = ui ^ (ui >> 16); /* defeat structure alignment */ + i = (int)(ui % pack_revindex_hashsz); + while (pack_revindex[i].p) { + if (pack_revindex[i].p == p) + return i; + if (++i == pack_revindex_hashsz) + i = 0; + } + return -1 - i; +} + +static void prepare_pack_ix(void) +{ + int num; + struct packed_git *p; + for (num = 0, p = packed_git; p; p = p->next) + num++; + if (!num) + return; + pack_revindex_hashsz = num * 11; + pack_revindex = xcalloc(sizeof(*pack_revindex), pack_revindex_hashsz); + for (p = packed_git; p; p = p->next) { + num = pack_revindex_ix(p); + num = - 1 - num; + pack_revindex[num].p = p; + } + /* revindex elements are lazily initialized */ +} + +static int cmp_offset(const void *a_, const void *b_) +{ + unsigned long a = *(unsigned long *) a_; + unsigned long b = *(unsigned long *) b_; + if (a < b) + return -1; + else if (a == b) + return 0; + else + return 1; +} + +/* + * Ordered list of offsets of objects in the pack. + */ +static void prepare_pack_revindex(struct pack_revindex *rix) +{ + struct packed_git *p = rix->p; + int num_ent = num_packed_objects(p); + int i; + void *index = p->index_base + 256; + + rix->revindex = xmalloc(sizeof(unsigned long) * (num_ent + 1)); + for (i = 0; i < num_ent; i++) { + long hl = *((long *)(index + 24 * i)); + rix->revindex[i] = ntohl(hl); + } + /* This knows the pack format -- the 20-byte trailer + * follows immediately after the last object data. + */ + rix->revindex[num_ent] = p->pack_size - 20; + qsort(rix->revindex, num_ent, sizeof(unsigned long), cmp_offset); +} + +static unsigned long find_packed_object_size(struct packed_git *p, + unsigned long ofs) +{ + int num; + int lo, hi; + struct pack_revindex *rix; + unsigned long *revindex; + num = pack_revindex_ix(p); + if (num < 0) + die("internal error: pack revindex uninitialized"); + rix = &pack_revindex[num]; + if (!rix->revindex) + prepare_pack_revindex(rix); + revindex = rix->revindex; + lo = 0; + hi = num_packed_objects(p) + 1; + do { + int mi = (lo + hi) / 2; + if (revindex[mi] == ofs) { + return revindex[mi+1] - ofs; + } + else if (ofs < revindex[mi]) + hi = mi; + else + lo = mi + 1; + } while (lo < hi); + die("internal error: pack revindex corrupt"); +} + static void *delta_against(void *buf, unsigned long size, struct object_entry *entry) { unsigned long othersize, delta_size; @@ -78,35 +227,69 @@ static unsigned long write_object(struct sha1file *f, struct object_entry *entry { unsigned long size; char type[10]; - void *buf = read_sha1_file(entry->sha1, type, &size); + void *buf; unsigned char header[10]; unsigned hdrlen, datalen; enum object_type obj_type; + int to_reuse = 0; - 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; - if (entry->delta) { - buf = delta_against(buf, size, entry); - size = entry->delta_size; - obj_type = OBJ_DELTA; + if (! entry->in_pack) + to_reuse = 0; /* can't reuse what we don't have */ + else if (obj_type == OBJ_DELTA) + to_reuse = 1; /* check_object() decided it for us */ + else if (obj_type != entry->in_pack_type) + to_reuse = 0; /* pack has delta which is unusable */ + else if (entry->delta) + to_reuse = 0; /* we want to pack afresh */ + else + to_reuse = 1; /* we have it in-pack undeltified, + * and we do not need to deltify it. + */ + + if (! to_reuse) { + buf = read_sha1_file(entry->sha1, type, &size); + if (!buf) + die("unable to read %s", sha1_to_hex(entry->sha1)); + if (size != entry->size) + die("object %s size inconsistency (%lu vs %lu)", + sha1_to_hex(entry->sha1), size, entry->size); + 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); - sha1write(f, header, hdrlen); - if (entry->delta) { - sha1write(f, entry->delta, 20); - hdrlen += 20; + else { + struct packed_git *p = entry->in_pack; + use_packed_git(p); + + datalen = find_packed_object_size(p, entry->in_pack_offset); + buf = p->pack_base + entry->in_pack_offset; + sha1write(f, buf, datalen); + unuse_packed_git(p); + hdrlen = 0; /* not really */ + if (obj_type == OBJ_DELTA) + reused_delta++; + reused++; } - datalen = sha1write_compressed(f, buf, size); - free(buf); + if (obj_type == OBJ_DELTA) + written_delta++; + written++; return hdrlen + datalen; } @@ -132,7 +315,6 @@ static void write_pack_file(void) int i; struct sha1file *f; unsigned long offset; - unsigned long mb; struct pack_header hdr; if (!base_name) @@ -148,8 +330,6 @@ static void write_pack_file(void) offset = write_one(f, objects + i, offset); sha1close(f, pack_file_sha1, 1); - mb = offset >> 20; - offset &= 0xfffff; } static void write_index_file(void) @@ -196,18 +376,20 @@ static int add_object_entry(unsigned char *sha1, unsigned int hash) { unsigned int idx = nr_objects; struct object_entry *entry; + struct packed_git *p; + unsigned int found_offset = 0; + struct packed_git *found_pack = NULL; - if (incremental || local) { - struct packed_git *p; - - for (p = packed_git; p; p = p->next) { - struct pack_entry e; - - if (find_pack_entry_one(sha1, &e, p)) { - if (incremental) - return 0; - if (local && !p->pack_local) - return 0; + for (p = packed_git; p; p = p->next) { + struct pack_entry e; + if (find_pack_entry_one(sha1, &e, p)) { + if (incremental) + return 0; + if (local && !p->pack_local) + return 0; + if (!found_pack) { + found_offset = e.offset; + found_pack = e.p; } } } @@ -221,30 +403,114 @@ static int add_object_entry(unsigned char *sha1, unsigned int hash) memset(entry, 0, sizeof(*entry)); memcpy(entry->sha1, sha1, 20); entry->hash = hash; + if (found_pack) { + entry->in_pack = found_pack; + entry->in_pack_offset = found_offset; + } nr_objects = idx+1; return 1; } +static int locate_object_entry_hash(unsigned char *sha1) +{ + int i; + unsigned int ui; + memcpy(&ui, sha1, sizeof(unsigned int)); + i = ui % object_ix_hashsz; + while (0 < object_ix[i]) { + if (!memcmp(sha1, objects[object_ix[i]-1].sha1, 20)) + return i; + if (++i == object_ix_hashsz) + i = 0; + } + return -1 - i; +} + +static struct object_entry *locate_object_entry(unsigned char *sha1) +{ + int i = locate_object_entry_hash(sha1); + if (0 <= i) + return &objects[object_ix[i]-1]; + return NULL; +} + static void check_object(struct object_entry *entry) { char type[20]; - if (!sha1_object_info(entry->sha1, type, &entry->size)) { - if (!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); + if (entry->in_pack) { + unsigned char base[20]; + unsigned long size; + struct object_entry *base_entry; + + /* We want in_pack_type even if we do not reuse delta. + * There is no point not reusing non-delta representations. + */ + check_reuse_pack_delta(entry->in_pack, + entry->in_pack_offset, + base, &size, + &entry->in_pack_type); + + /* Check if it is delta, and the base is also an object + * we are going to pack. If so we will reuse the existing + * delta. + */ + if (!no_reuse_delta && + entry->in_pack_type == OBJ_DELTA && + (base_entry = locate_object_entry(base))) { + + /* Depth value does not matter - find_deltas() + * will never consider reused delta as the + * base object to deltify other objects + * against, in order to avoid circular deltas. + */ + + /* uncompressed size of the delta data */ + entry->size = entry->delta_size = size; + entry->delta = base_entry; + entry->type = OBJ_DELTA; + + base_entry->edge = 1; + + return; + } + /* Otherwise we would do the usual */ } - else + + if (sha1_object_info(entry->sha1, type, &entry->size)) die("unable to get type of object %s", sha1_to_hex(entry->sha1)); + + if (!strcmp(type, "commit")) { + entry->type = OBJ_COMMIT; + } else if (!strcmp(type, "tree")) { + 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) @@ -252,6 +518,8 @@ static void get_object_details(void) int i; struct object_entry *entry = objects; + hash_objects(); + prepare_pack_ix(); for (i = 0; i < nr_objects; i++) check_object(entry++); } @@ -326,6 +594,13 @@ static int try_delta(struct unpacked *cur, struct unpacked *old, unsigned max_de if (cur_entry->type != old_entry->type) return -1; + /* If the current object is at edge, take the depth the objects + * that depend on the current object into account -- otherwise + * they would become too deep. + */ + if (cur_entry->edge) + max_depth /= 4; + size = cur_entry->size; if (size < 50) return -1; @@ -382,11 +657,19 @@ static void find_deltas(struct object_entry **list, int window, int depth) eye_candy -= nr_objects / 20; fputc('.', stderr); } + + if (entry->delta) + /* This happens if we decided to reuse existing + * delta from a pack. "!no_reuse_delta &&" is implied. + */ + continue; + free(n->data); n->entry = entry; n->data = read_sha1_file(entry->sha1, type, &size); if (size != entry->size) die("object %s inconsistent object length (%lu vs %lu)", sha1_to_hex(entry->sha1), size, entry->size); + j = window; while (--j > 0) { unsigned int other_idx = idx + j; @@ -411,10 +694,12 @@ static void find_deltas(struct object_entry **list, int window, int depth) static void prepare_pack(int window, int depth) { - get_object_details(); - if (progress) fprintf(stderr, "Packing %d objects", nr_objects); + get_object_details(); + if (progress) + fputc('.', stderr); + sorted_by_type = create_sorted_list(type_size_sort); if (window && depth) find_deltas(sorted_by_type, window+1, depth); @@ -443,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, - sha1_to_hex(sha1)); + if (progress) + fprintf(stderr, "Reusing %d objects pack %s\n", nr_objects, + sha1_to_hex(sha1)); if (pack_to_stdout) { if (copy_fd(ifd, 1)) @@ -524,6 +810,10 @@ int main(int argc, char **argv) progress = 0; continue; } + if (!strcmp("--no-reuse-delta", arg)) { + no_reuse_delta = 1; + continue; + } if (!strcmp("--stdout", arg)) { pack_to_stdout = 1; continue; @@ -599,5 +889,8 @@ int main(int argc, char **argv) puts(sha1_to_hex(object_list_sha1)); } } + if (progress) + fprintf(stderr, "Total %d, written %d (delta %d), reused %d (delta %d)\n", + nr_objects, written, written_delta, reused, reused_delta); return 0; } diff --git a/pack.h b/pack.h index 9dafa2b6d2..694e0c56f0 100644 --- a/pack.h +++ b/pack.h @@ -29,5 +29,7 @@ struct pack_header { }; extern int verify_pack(struct packed_git *, int); - +extern int check_reuse_pack_delta(struct packed_git *, unsigned long, + unsigned char *, unsigned long *, + enum object_type *); #endif diff --git a/sha1_file.c b/sha1_file.c index 64cf245418..0a3a721e97 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -826,6 +826,25 @@ static unsigned long unpack_object_header(struct packed_git *p, unsigned long of return offset; } +int check_reuse_pack_delta(struct packed_git *p, unsigned long offset, + unsigned char *base, unsigned long *sizep, + enum object_type *kindp) +{ + unsigned long ptr; + int status = -1; + + use_packed_git(p); + ptr = offset; + ptr = unpack_object_header(p, ptr, kindp, sizep); + if (*kindp != OBJ_DELTA) + goto done; + memcpy(base, p->pack_base + ptr, 20); + status = 0; + done: + unuse_packed_git(p); + return status; +} + void packed_object_info_detail(struct pack_entry *e, char *type, unsigned long *size,