threads / discuss / 737

More gitweb queries..

Subject: More gitweb queries..

## tl;dr

42 messages between May 27, 2005 and May 31, 2005.

replies: 41people: 9as markdown or json

Linus Torvalds· May 27, 2005, 19:24 UTC · lore
Kay,
 just a few more quickie suggestions if you don't mind..
 - looking around, the ALSA guys aren't the only ones that start off with 
   an empty line, so it's probably worth fixing the summary etc to ignore 
   whitespace at the beginning rather than give empty summary reasons.
 - any reason to limit the "summary" page to just the last 14 changes? The 
   "log" thing you can ask to go back further, it would be nice to have
   something like a "last 100" thing for summaries too, especially since 
   the summary is so nice and dense, so you can actually get a nice view 
   of what has happened without scrolling _too_ much.
 - I was in the "commitdiff" thing, and initially thought that there was 
   no way to get back to the "summary" view.
   It turns out I was wrong (the summary is reachable by just clicking at
   the project name itself in the top header), but it's a bit strange that
   the "commitdiff" thing has an explicit link back to itself (hey, 
   consistency is good, so I'm not complaining), but the link back to the
   summary page is implicit.
   So how about adding an explicit "summary" link to the list of other 
   explicit links (log, commit, commitdiff and tree) at the top of the 
   page?

I actually like browsing other peoples projects with the gitweb interfaces, it's both responsive and verbose enough to really say something good. In contrast, cvsweb is always just a mess of "these are the files, go at it", which is totally pointless and doesn't tell anything about what is actually happening in the project.

So dammit, I'm very biased indeed, but I'm just looking at gitweb, and comparing it to both the CVS and SVN web things, and they just reinforce my conviction that CVS is absolute crap, and I find myself surprised by how crap SVN also appears.

[ In other words, while I have all these stupid requests for you, I 
  think gitweb is just _way_ better and more useful than something like
  viewcvs (or cvsweb). So don't mind my small gripes, I only have them 
  because I like this thing.
  On that small note, I also find "gitk" very cool indeed, too bad about 
  the fact that tk/tcl seems to always end up looking so _ugly_. Is there 
  any way to get anti-aliased fonts and a less 'Motify' blocky look from 
  tcl/tk? Every time I see that, I feel like I'm back in the last century  
  or something.
  Combining some of the features of the two (that über-cool revision 
  history graph from gitk rules, for example) might be cool. I get the 
  urge to do octopus-merges in the kernel just because of how good they
  look in gitk ;) ]
			Linus
Thomas Glanzmann· May 27, 2005, 19:29 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

Hello,
> I get the urge to do octopus-merges in the kernel just because of how
> good they look in gitk ;) ]

talking about octopus-merges ... I don't understand how they work. What happens if one file is touched in every of the 8 trees. How can that be handled?

	Thomas
Junio C Hamano· May 27, 2005, 19:52 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

>>>>> "TG" == Thomas Glanzmann <sithglan@stud.uni-erlangen.de> writes:

TG> talking about octopus-merges ... I don't understand how they work. What TG> happens if one file is touched in every of the 8 trees. How can that be TG> handled?

You merge by hand and resolve if they have conflicts, just like what you already do in two head merge case.

Octopus is only about how you record the results. Instead of making 7 consecutive "merge from A" "merge from B" to record two head merges, you just say "I merged these 8 heads" in a single commit.

Thomas Glanzmann· May 27, 2005, 19:55 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

Hello,
> You merge by hand and resolve if they have conflicts, just like
> what you already do in two head merge case.

I see. Does that mean that 'git-ls-files --unmerged' will report upto 9 stages per file?

> Octopus is only about how you record the results.  Instead of
> making 7 consecutive "merge from A" "merge from B" to record two
> head merges, you just say "I merged these 8 heads" in a single
> commit.
I got that part. :-)
	Thomas
Junio C Hamano· May 27, 2005, 20:13 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

>>>>> "TG" == Thomas Glanzmann <sithglan@stud.uni-erlangen.de> writes:
>> You merge by hand and resolve if they have conflicts, just like
>> what you already do in two head merge case.

TG> I see. Does that mean that 'git-ls-files --unmerged' will report upto 9 TG> stages per file?

No, I think my description was unclear. You still merge two at a time because that is what git-read-tree -m gives you (3-way merge is between $(merge-base $A $B) and $A and $B so you are merging two heads).

To confess, my workflow to merge with Linus is currently primarily patch based, so I do not even use git-read-tree -m 3-way merge when I make an Octopus (for that matter, I do not myself do Octopus at all these days). When I have bunch of independent changes, I would first prepare and test these:

           -- JC#1
          / - JC#2
         /  - JC#3
  Linus#1-  - JC#4  
         \    ...
          \-- JC#7

By the time I am done and happy with them, tip of Linus tree may have already advanced and he is at Linus#2. I would then apply diffs between Linus#1 and JC#n (1 <= n <= 7) on top of Linus #2, and commit the result with parents set to Linus #2 and JC#1, JC#2, ..., JC#7.

           ------- Linus#2
          /               \
         / -- JC#1 --------\
        / /-- JC#2 ---------\ 
       / /--- JC#3 ----------\
  Linus#1---- JC#4 ---------- Octopus 
         \    ...            /
          \-- JC#7 ----------
Linus Torvalds· May 27, 2005, 20:17 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

On Fri, 27 May 2005, Thomas Glanzmann wrote:
Show 5 quoted lines
> > You merge by hand and resolve if they have conflicts, just like
> > what you already do in two head merge case.
> 
> I see. Does that mean that 'git-ls-files --unmerged' will report upto 9
> stages per file?

No, you must always merge trees one by one against each other. The simplest ordering is to merge trees 1-2 first, then the result of that with 3, then the result of _that_ with 4 etc etc, but you can - if you really want to - do 1-2 and 3-4 separately and then merge those two together.

The ordering does actually end up mattering a bit when it comes to deciding on parenthood, but in the end you will have used the same most remote common parent for _one_ of the merges anyway, so assuming all the merges were automatically resolved by the regular 3-way thing, I claim that it doesn't really matter noticeably (*).

Regardless, you'd end up with seven "git-read-tree -m x y z" invocations (plus possibly a few git-merge-cache calls), and one final commit.

		Linus

(*) I bet you could find some case where the ordering either generates a create-create conflict or it doesn't, depending on how you pair things up.

But I also claim that you'd be crazy to do a octopus merge for something like that anyway, and that the reason to do one is that you've had five totally disjoint things you've been working on - like updating five different drivers or five different filesystems in different branches, and there are no conflicts however you turn.

Junio C Hamano· May 27, 2005, 19:54 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Thomas, could you please stop doing Mail-Followup-To in your header please? I automatically did 'reply all' and ended up preaching Linus (because that was the first mailbox on your Mail-Followup-to header) how Octopus works, when he knows what it is already.

Thomas Glanzmann· May 27, 2005, 19:58 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

Hello,
* Junio C Hamano <junkio@cox.net> [050527 21:54]:
Show 5 quoted lines
> Thomas, could you please stop doing Mail-Followup-To in your
> header please?  I automatically did 'reply all' and ended up
> preaching Linus (because that was the first mailbox on your
> Mail-Followup-to header) how Octopus works, when he knows what
> it is already.
test without the mft.
	Thomas
Linus Torvalds· May 27, 2005, 20:03 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

On Fri, 27 May 2005, Thomas Glanzmann wrote:
Show 7 quoted lines
> 
> > I get the urge to do octopus-merges in the kernel just because of how
> > good they look in gitk ;) ]
> 
> talking about octopus-merges ... I don't understand how they work. What
> happens if one file is touched in every of the 8 trees. How can that be
> handled?
Automatically? You can do multiple three-way merges, no problem. 

In fact, the general algorithm for an n-way merge is to just do the "git-resolve-script" n-1 times, but _without_ the commit. Then you just commit the result, and the only thing to keep in mind is to get the parents right, because if you don't, you're screwed.

This does imply a merge ordering, but since we order the parents anyway, that's actually also described 100% by the commit, so the end result is clean and good.

There are two reasons not to do octopus-merges, and neither of them is huge, but they've kept me from doing them..

 - if you screw up half-way through the merge, it's a lot harder to 
   recover without blowing away all the other merges too and having to 
   re-do them. You certainly _can_ do it (say, by just recording the trees
   in between merges - it's definitely not rocket science), but it
   basically means that you need to keep track of things _outside_ of the
   normal "what was the last HEAD" model.
   More importantly, since an octopus merge has only one commit message 
   associated with it, you really should never use one for anything that 
   needs any manual intervention. Otherwise you'll have to start 
   explaining which merge you needed to fix up manually etc, and it just 
   gets complex for no actual gain.
   IOW, this argument is only against complex merges. The trivial ones can 
   easily be done as octopuses, and in many ways the resulting history may 
   actually reflect what you did better. For example, for somebody like 
   Jeff, who maintains 50 different branches, and merges 5 of them to send 
   them to me, an octopus merge in many ways is much more intuitive: it 
   really says "I took these five branches and combined them", while a 
   series of four regular merges just gets messy.
 - Compatibility with other systems. 
   I don't care one whit about stuff I consider broken (ie CVS), but there 
   are SCM's out there that I _don't_ think are broken, and that don't do
   multi-parent merges for "nrparent > 2".  You can always split an 
   octopus merge that didn't have any manual intervention, so again, this
   is not a huge argument if you follow rule #1, but unless you have a 
   reason for doing an octopus merge, it means that you should probably 
   avoid it.
   So _I_ usually don't have any reason at all, it would be stupid of me
   to merge trees from different people as an octopus, but usage like 
   Jeff's (where the merge is due to "pass these <n> trees upwards") is 
   different.

So there you have it. Don't do it just because you can, but if you have a good reason for them and they were done automatically without any human intervention (apart from having to change the scripts, of course), I won't argue too much against them either. I already took one such merge from Junio in the GIT tree, and I actually like having that as a way to make sure the tools can handle it.

		Linus
Junio C Hamano· May 27, 2005, 20:24 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

>>>>> "LT" == Linus Torvalds <torvalds@osdl.org> writes:

LT> For example, for somebody like LT> Jeff, who maintains 50 different branches, and merges 5 of them to send LT> them to me, an octopus merge in many ways is much more intuitive: it LT> really says "I took these five branches and combined them", while a LT> series of four regular merges just gets messy.

This really is a good use case for an Octopus. I hope Jeff is reading this thread.

Thomas Glanzmann· May 27, 2005, 20:32 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

Hello, okay thanks for the elaboration on the topic. I will now adopt my scripts to handle it. I think I already have a use for it.

            -- mutt-hcache   --
           /-- mutt-imap     --\
          /--- mutt-whatever ---\
 mutt-cvs ---- ...           ----- mutt-tg (my working tree)
          \    ...           ----/
           \-- ...             -/
Actually, I have already 12 trees with different features which I work on.
 1 mutt-attach-file       5 mutt-hcache            9 mutt-menu-move
 2 mutt-collapse-flags    6 mutt-headers          10 mutt-move-hook
 3 mutt-cstatus           7 mutt-imap             11 mutt-setenv-hack
 4 mutt-edit-threads      8 mutt-maildir-mtime    12 mutt-thread-pattern

But I guess 8 is the limit, isn't it? Did you thought to make this 8 a 'n' or is 8 just enough? :-)

	Thomas
Junio C Hamano· May 27, 2005, 20:40 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

>>>>> "TG" == Thomas Glanzmann <sithglan@stud.uni-erlangen.de> writes:

TG> But I guess 8 is the limit, isn't it? Did you thought to make this 8 a TG> 'n' or is 8 just enough? :-)

Built-in limit of commit object is 16, not 8.
Linus Torvalds· May 27, 2005, 22:00 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

On Fri, 27 May 2005, Junio C Hamano wrote:
Show 7 quoted lines
>
> >>>>> "TG" == Thomas Glanzmann <sithglan@stud.uni-erlangen.de> writes:
> 
> TG> But I guess 8 is the limit, isn't it? Did you thought to make this 8 a
> TG> 'n' or is 8 just enough? :-)
> 
> Built-in limit of commit object is 16, not 8.

Actually, even that is not actually built into the commit object itself, that's just a #define in commit-tree.c.

Change the MAXPARENT design from 16 to 1024, and nobody will notice any difference at all, except "git-commit-tree.c" will use 20kB more memory ;)

There's no limit in the data structures, although there clearly is a "sanity" limit (and I personally suspect it comes before you hit 16 ;)

		Linus
Thomas Glanzmann· May 27, 2005, 22:04 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

Hello,
> Actually, even that is not actually built into the commit object
> itself, that's just a #define in commit-tree.c.  Change the MAXPARENT
> design from 16 to 1024, and nobody will notice any difference at all,
> except "git-commit-tree.c" will use 20kB more memory ;)
That sounds just way to perfect. :-)
> There's no limit in the data structures, although there clearly is a 
> "sanity" limit (and I personally suspect it comes before you hit 16 ;)

I like how this 'simple' git concepts just fits into all this usage scenarios. Including this one or the way we can track renames. Name it! Thanks for giving us this perfect piece of software! :-)

	Thomas
Junio C Hamano· May 28, 2005, 02:26 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

>>>>> "LT" == Linus Torvalds <torvalds@osdl.org> writes:

LT> There's no limit in the data structures, although there clearly is a LT> "sanity" limit (and I personally suspect it comes before you hit 16 ;)

I know that my head would start hurting way before I hit 16.

I probably shouldn't have coined the word Octopus in the first place, giving people a false impression that somehow 8 is a magic number. To begin with, what I inflicted on you was not even an Octopus but a Pentapus, merge of 5 IIRC.

Also we are counting heads, not legs. Should have said Hydra, but I do not offhand know how many heads it has --- I've never met one. I know King Ghidorah has 3 heads ;-).

Thomas Glanzmann· May 29, 2005, 23:02 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Hello, here we go! gitk screenshot: http://wwwcip.informatik.uni-erlangen.de/~sithglan/shot.png (64k)

(faui00u) [~/work/mutt/git/mutt-test] ../../../git/yagf/git pull ../mutt-attach-file/ ../mutt-collapse-flags/ ../mutt-cstatus/ ../mutt-cvs/ ../mutt-edit-threads/ ../mutt-hcache/ ../mutt-headers/ ../mutt-imap/ ../mutt-maildir-mtime/ ../mutt-move-hook/ ../mutt-setenv-hack/ ../mutt-thread-pattern/ ../mutt-menu-move/
<chatty output>
(faui00u) [~/work/mutt/git/mutt-test] git treediff ../mutt-tg-solaris
(faui00u) [~/work/mutt/git/mutt-test] git changes -m | perl -pe '$a += /^diff-tree/; exit if $a==2'
diff-tree 5b22da9792b6f6a968dc0a916275d6c301575f75 (from e8f4a291a81f0a8fb24555f0e36e4b75e2d3f4c8)
Author: Thomas Glanzmann <sithglan@stud.uni-erlangen.de>
Date:   Mon May 30 00:28:53 2005 +0200
    => faui00u:/home/cip/adm/sithglan/work/mutt/git/mutt-test
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-attach-file/.git (bringing head ahead)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-collapse-flags/.git (automatic merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-cstatus/.git (threeway merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-cvs/.git (nothing to merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-edit-threads/.git (threeway merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-hcache/.git (threeway merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-headers/.git (threeway merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-imap/.git (automatic merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-maildir-mtime/.git (threeway merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-move-hook/.git (nothing to merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-setenv-hack/.git (automatic merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-thread-pattern/.git (threeway merge)
    <= /home/cip/adm/sithglan/work/mutt/git/mutt-test/../mutt-menu-move/.git (threeway merge)

Could please someone who has a clue how this merge should work (which it hopefully already does) look at my code to check for obvious mistakes in the merge *logic*. I call 'merge' with all involved trees (with current head first, if there is a current head). If everything is fine. I would like to write a seperate git-resolve script in sh or adopt the current to multi head merge - whatever you please.

sub merge {

	my $message      = undef;
	my $head         = undef;
	my $last_tree    = undef;
	my $fh;
	my @heads        = ();
	foreach my $r (@_) {
		my $current_head = @{$r}[0];
		my $current_url  = @{$r}[1];
		print "current_head => $current_head\ncurrent_url => $current_url\n";
		push(@heads, '-p', ${current_head});
		if (! defined($last_tree)) {
			$message    = "=> ${current_url}\n";
			$head       = $current_head;
			$last_tree  = $current_head;
			
			if (@_ == 1) {
				head($head);
				return;
			}
			next;
		}
		my $merge_base = gitcmdout('git-merge-base', $head, $current_head)
				 || die ("no merge-base");
		chomp($merge_base);
		print "head => $head\nremote => $current_head\nbase => $merge_base\n";
	
		if ($merge_base eq $current_head) {
			$message .= "<= ${current_url} (nothing to merge)\n";
			if (@_ == 2) {
				return;
			}
			next;
		}
		if ($merge_base eq $head) {
			$message   .= "<= ${current_url} (bringing head ahead)\n";
			$head       = ${current_head};
			$last_tree  = ${current_head};
			if (@_ == 2) {
				head($head);
				return;
			}
			next;
		}
		gitcmd('git-read-tree', '-m', $merge_base, $last_tree, $current_head);
		if (! defined($last_tree = write_tree())) {
			system('git-merge-cache', '-o', 'git-merge-one-file-script', '-a');
			if (! defined($last_tree = write_tree())) {
				# FIXME: Make manual intervention possible
				# --tg 23:11 05-05-29
				die("Couldn't merge automatically: Call 'git resolve'");
			}
			$message .= "<= ${current_url} (threeway merge)\n";
		} else {
			$message .= "<= ${current_url} (automatic merge)\n";
		}
	}
	open($fh, "+>", undef);
	print $fh $message;
	seek($fh, 0, 0);
	$head = gitcmdinout($fh, 'git-commit-tree', $last_tree, @heads);
	chomp($head);
	close $fh;
	head($head);
	return;
}
	Thomas
#!/usr/bin/env perl

use strict; use warnings; use IO::Handle; use File::Temp qw/ tempfile tempdir /; use File::Copy; use Cwd; use Getopt::Long;

STDOUT->autoflush(1);

my $hostname = gitcmdout('hostname'); chomp($hostname);

my $DIFF = undef; my $PATCH = undef;

if (-x '/opt/csw/bin/gdiff') {
	$DIFF='/opt/csw/bin/gdiff';
} else {
	$DIFF='diff';
}
if (-x '/usr/bin/gpatch') {
	$PATCH='/usr/bin/gpatch';
} else {
	$PATCH='patch';
}
my %commands = (
	"add"        => \&add,
	"checkout"   => \&checkout,
	"ci"         => \&ci,
	"clone"      => \&clone,
	"commit"     => \&commit,
	"diff"       => \&diff,
	"treediff"   => \&treediff,
	"dirty"      => \&dirty,
	"help"       => \&usage,
	"init-db"    => \&init_db,
	"log"        => \&log,
	"orphan"     => \&orphan,
	"parent"     => \&parent,
	"patch"      => \&patch,
	"pull"       => \&pull,
	"push"       => \&_push,
	"refresh"    => \&refresh,
	"revert"     => \&revert,
	"rm"         => \&rm,
	"setup"      => \&setup,
	"status"     => \&status,
	"undo"       => \&undo,
	"changed"    => \&changed,
	"resolve"    => \&resolve,
	"changes"    => \&changes,
);
my %touched = ();
sub
usage
{
	print STDERR <<"__EOF__";
Usage: $0 COMMAND [ARG]...
Available commands:
__EOF__
	print "\t" . join("\n\t", sort(keys(%commands))) . "\n\n";
	return 1;
}
sub
refresh
{
	`git-update-cache --refresh`;
}
sub
process_git_diff_output
{
	my $str = shift || return (());
	my @in  = split("\0", $str);
	my @out = ();
	while (@in) {
		my @tmp = split(' ', shift(@in));
		$tmp[0] =~ s/^://g;
		push(@tmp, shift(@in));
		push(@out, [@tmp]);
	}
	return(@out);
}
sub
dirty_files
{
	refresh();
	my @dirty = ();
	my $str = gitcmdout('git-diff-files', '-z', '-r');
	foreach (process_git_diff_output($str)) {
		if ((@{$_})[1] ne '000000') {
			push(@dirty, @{$_}[5]);
		}
		#print "<" . join("> <", @{$_}) . ">\n";
	}
	return @dirty;
}
sub
orphan_files
{
	my @orphan = gitcmdout('git-ls-files', '--others');
	chomp(@orphan);
	my $regexp = '^\.';
	if (-f '.git/ignore') {
		my @ignore = ();
		chomp (@ignore = _read_file('.git/ignore'));
		$regexp = join('|', '^\.', @ignore);
	}
	@orphan = grep(! /$regexp/, @orphan);
	return @orphan;
}
sub
orphan
{
	foreach (orphan_files()) {
		print $_ . "\n";
	}
}
sub
dirty
{
	foreach (dirty_files()) {
		print $_ . "\n";
	}
}
sub
changed
{
	foreach my $rev (gitcmdout('git-rev-list', 'HEAD')) {
		chomp($rev);
		my %hash = commit_hash($rev);
		
		my $str = gitcmdout('git-diff-tree', '--root', '-r', '-z', $rev);
		foreach (process_git_diff_output($str)) {
			my ($time, $rest) = split(/\s/, $hash{committer_date});
			push(@{$touched{@{$_}[5]}}, $time);
		}
	}
}
sub
status
{
	my %hash = ();
	
	foreach (orphan_files()) {
		$hash{$_} = '?';
	}
	foreach (dirty_files()) {
		$hash{$_} = 'D';
	}
	my $str = gitcmdout('git-diff-cache', '-r', '--cached', '-z', 'HEAD');
	foreach (process_git_diff_output($str)) {
		if      (@{$_}[0] eq '000000') {
			$hash{@{$_}[5]} .= '+';
		} elsif (@{$_}[1] eq '000000') {
			$hash{@{$_}[5]} .= '-';
		} else {
			$hash{@{$_}[5]} .= '*';
		}
	}
	foreach (sort(keys(%hash))) {
		if ($hash{$_} eq "D") {
			$hash{$_} = "D ";
		}
		printf("% 2s %s\n", $hash{$_}, $_);
	}
}
sub
write_tree
{
	chomp (my $tree = `git-write-tree`);
	if ($?) {
		return undef;
	} else {
		return $tree;
	}
}
sub
retrieve_unmerged
{
	my %hash = ();
	
	foreach my $line (gitcmdout('git-ls-files', '--unmerged')) {
		chomp($line);
# 100644 cde27275fad8103084d7ed2d08d246ba4ce6eb9c 1 Makefile
# 100644 d311a35e5e8a09629ea9e6051a43710c76fa8f6d 2 Makefile
# 100644 ec2b76bf90fb105b2aaf00a66f44b135046d3002 3 Makefile
		if ($line =~ /^(\d{6})\s([a-z0-9]{40})\s(\d)\s(.+)$/) {
			push(@{$hash{$4}}, $line);
		} else {
			die("Can't match: <$line>\n");
		}
	}
	return %hash;
}
sub
process_unmerged_file
{
	my $line;
	my $file;
	my @files = ();
	while($line = shift) {
		if ($line =~ /^(\d{6})\s([a-z0-9]{40})\s(\d)\s(.+)$/) {
			if ($3 eq '1') { $file = "$4.GCA"; }
			if ($3 eq '2') { $file = "$4.LOCAL"; }
			if ($3 eq '3') { $file = "$4.REMOTE"; }
			my $mode = substr($1, 2);
			if (-f $file) {
				die("Please get rid of $file\n");
			}
			print STDERR "Checking out: $file with permissions $mode\n";
			system("git-cat-file blob $2 > $file");
			chmod oct($mode), $file;
			push(@files, $file);
		}
	}
	if (@files == 3) {
		my $filename = (@files)[0];
		$filename =~ s#\.(GCA|LOCAL|REMOTE)$##;
		print STDERR <<"EOF";

You got GCA, LOCAL and REMOTE, so I run merge for you and leaving the merges in ${filename} .\n

EOF
		unlink($filename);
		system('cp', "${filename}.LOCAL", "${filename}");
		system('merge', "${filename}", "${filename}.GCA", "${filename}.REMOTE");
	}
	print STDERR <<"EOF";

Please resolve the conflict and add the file using the command 'git ci file'. Droping you to a login shell now. Please exit the shell as soon as you resolved the conflict.

EOF
	system($ENV{'SHELL'}, '--login');
	foreach (@files) {
		unlink($_);
	}
}
sub
resolve
{
	if (! -f '.git/RESOLVE') {
		die("Nothing to resolve");
	}
	if ( ! -f '.git/HEAD' && ! -l '.git/HEAD') {
		die("How the hell I am supposed to resolve without a head?");
	}
	my $fh;
	my $head = head();
	my $merge_tree = undef;
	my $pwd        = getcwd;
	chomp (my ($remote_head, $url) = _read_file('.git/RESOLVE'));
	print "remote_head => $remote_head\nurl => $url\n";
	my %unmerged = retrieve_unmerged();
	foreach my $file (keys(%unmerged)) {
		process_unmerged_file(@{$unmerged{$file}});
	}
	if (! defined($merge_tree = write_tree())) {
		die("Still unresolved conflicts. Run git resolve again.");
	}
	open($fh, "+>", undef);
	print $fh "Manual Merge $url => ${hostname}:${pwd}\n";
	seek($fh, 0, 0);
	chomp($head = gitcmdinout($fh, 'git-commit-tree', $merge_tree, '-p', $head, '-p', $remote_head));
	close $fh;
	head($head);
	unlink('.git/RESOLVE');
	checkout('-f');
	print STDERR "All issues resolved manual: Commited as ${head}.\n";
        print STDERR "Have a pleasant day.\n";
	return;
}

sub merge {

	my $message      = undef;
	my $head         = undef;
	my $last_tree    = undef;
	my $fh;
	my @heads        = ();
	foreach my $r (@_) {
		my $current_head = @{$r}[0];
		my $current_url  = @{$r}[1];
		print "current_head => $current_head\ncurrent_url => $current_url\n";
		push(@heads, '-p', ${current_head});
		if (! defined($last_tree)) {
			$message    = "=> ${current_url}\n";
			$head       = $current_head;
			$last_tree  = $current_head;
			
			if (@_ == 1) {
				print "Setting HEAD\n";
				head($head);
				return;
			}
			print "Next round\n";
			next;
		}
		my $merge_base = gitcmdout('git-merge-base', $head, $current_head)
				 || die ("no merge-base");
		chomp($merge_base);
		print "head => $head\nremote => $current_head\nbase => $merge_base\n";
	
		if ($merge_base eq $current_head) {
			$message .= "<= ${current_url} (nothing to merge)\n";
			if (@_ == 2) {
				return;
			}
			next;
		}
		if ($merge_base eq $head) {
			$message   .= "<= ${current_url} (bringing head ahead)\n";
			$head       = ${current_head};
			$last_tree  = ${current_head};
			if (@_ == 2) {
				head($head);
				return;
			}
			next;
		}
		gitcmd('git-read-tree', '-m', $merge_base, $last_tree, $current_head);
		if (! defined($last_tree = write_tree())) {
			system('git-merge-cache', '-o', 'git-merge-one-file-script', '-a');
			if (! defined($last_tree = write_tree())) {
				# FIXME: Make manual intervention possible
				# --tg 23:11 05-05-29
				die("Couldn't merge automatically: Call 'git resolve'");
			}
			$message .= "<= ${current_url} (threeway merge)\n";
		} else {
			$message .= "<= ${current_url} (automatic merge)\n";
		}
	}
	open($fh, "+>", undef);
	print $fh $message;
	seek($fh, 0, 0);
	$head = gitcmdinout($fh, 'git-commit-tree', $last_tree, @heads);
	chomp($head);
	close $fh;
	head($head);
	return;
}
sub
patch
{
	my @files = ();
	my ($fh, $patch);
	my $file = shift || die("Need at least on argument.\n");
	my $head  = undef;
	my $dir   = undef;
	if (head()
	&& (    dirty_files()
            || `git-diff-cache -r --cached HEAD`)) {
		print STDERR "Get rid of dirty files / uncommited deltas first.\n";
		exit 1;
	}
	($fh, $patch) = tempfile(CLEANUP => 1);
	`filterdiff -x '*/.*' $file > $patch`;
	@files = `lsdiff --strip 1 $patch`;
	chomp(@files);
	$dir = tempdir(CLEANUP => 1);
	$ENV{GIT_INDEX_FILE} = "$dir/.index";
	if ($head = head()) {
		gitcmd('git-read-tree', $head);
		foreach my $file (@files) {
			gitcmd('git-checkout-cache', '-q', "--prefix=$dir/", $file);
		}
	}
	my $pwd = getcwd;
	symlink("$pwd/.git", "$dir/.git");
	chdir($dir);
	# FIXME call in batch modus and check return value --tg 08:30 05-05-21
	system("${PATCH} -p1 < $patch");
	foreach my $file (@files) {
		gitcmd('git-update-cache', '--add', '--remove', $file);
	}
	commit();
	chdir($pwd);
	delete($ENV{GIT_INDEX_FILE});
	checkout('-f');
}
sub
checkout
{
	my $head = head();
	gitcmd('git-read-tree', '-m', $head);
	# FIXME if this fails call it without -m --tg 20:25 05-05-09
	if (defined($_[0]) && $_[0] eq '-f') {
		gitcmd('git-checkout-cache', '-u', '-f', '-a');
	} else {
		gitcmd('git-checkout-cache', '-u', '-q', '-a');
	}

# changed(); # # foreach my $file (gitcmdout('git-ls-files')) { # chomp($file); # my $time = (sort {$b <=> $a} @{$touched{$file}})[0]; # utime $time, $time, $file; # } }

sub
generate_url
{
	my $url = shift;
	if (! defined($url)) {
		if ( -f '.git/PARENT' || -l '.git/PARENT') {
			chomp ($url = _read_file('.git/PARENT'));
		} else {
			die("$0: No URL specified and no parent found: Where to pull from?\n");
		}
	} else {
		$url =~ s#\/$##;
		if (-d $url &&
		    ! ($url =~ /\.git$/)) {
				$url .= "/.git";
		}
		if (-d $url &&
		    ! ($url =~ /^\//)) {
			# FIXME: use Cwd; my $pwd = getcwd; --tg 21:32 05-05-03
			chomp(my $pwd = `pwd`);
			$url = "${pwd}/${url}";
		}
	}
	return $url;
}
sub
pull
{
	my @references = ();
	my %options;
	local @ARGV = @_;
	GetOptions(\%options, 'o', 'l');
	@_ = @ARGV;
	if (head()
	&& (! defined($options{'o'}))
	&& (    dirty_files()
	    || `git-diff-cache -r --cached HEAD`)) {
		print STDERR "Get rid of dirty files / uncommited deltas first.\n";
		exit 1;
	}
	# FIXME SANITY CHECK. It is only possible to pull in 15 trees
	# --tg 17:58 05-05-28
	if (! defined($_[0])) {
		unshift(@_, generate_url(shift));
	}
	if ( -f '.git/HEAD' || -l '.git/HEAD') {
		push(@references, [head(), "${hostname}:" . getcwd()]);
	}
	while(defined (my $url = shift)) {
		$url = generate_url($url);
		my $remote_head = undef;
		if (defined($options{'l'}) && -d $url) {
			$ENV{'GIT_ALTERNATE_OBJECT_DIRECTORIES'} = "${url}/objects";
		} else {
			gitcmd('rsync', '-qa', '--ignore-existing', "$url/objects/.", ".git/objects/.");
		}
		gitcmd('rsync', '-qL', "$url/HEAD", '.git/REMOTE_HEAD');
		chomp($remote_head = _read_file('.git/REMOTE_HEAD'));
		push(@references, [$remote_head, $url]);
	}
	if (! defined($options{'o'})) {
		merge(@references);
		checkout('-f');
	}
}
sub
changes
{
	my %options;
	local @ARGV = @_;
	GetOptions(\%options, "L", "R", "d", "m", "n", "t=s", "S=s", 'root');
	@_ = @ARGV;
	my $git_diff_tree_options = '-s';
	if (defined ($options{'d'})) {
		$git_diff_tree_options = '-p';
	}
	if (defined ($options{'m'})) {
		$git_diff_tree_options .= ' -m';
	}
	if (defined ($options{'S'})) {
		$git_diff_tree_options .= " -S'$options{'S'}'";
	}
	if (defined ($options{'root'})) {
		$git_diff_tree_options .= " --root";
	}
	if (defined ($options{'L'})) {
		if (! defined ($options{'n'})) {
			pull('-o', '-l', shift);
		}
		system("git-rev-tree HEAD '^REMOTE_HEAD' | sed -e 's/^[0-9]* //' | git-diff-tree --stdin -v $git_diff_tree_options " . join(' ', @ARGV));
	} elsif (defined ($options{'R'})) {
		if (! defined ($options{'n'})) {
			pull('-o', '-l', shift);
		}
		system("git-rev-tree REMOTE_HEAD '^HEAD' | sed -e 's/^[0-9]* //' | git-diff-tree --stdin -v $git_diff_tree_options " . join(' ', @ARGV));
	} elsif (defined ($options{'t'})) {
		system("git-rev-tree HEAD '^$options{'t'}' | sed -e 's/^[0-9]* //' | git-diff-tree --stdin -v $git_diff_tree_options " . join (' ', @ARGV));
	
	} else {
		system("git-rev-list HEAD | git-diff-tree --stdin -v $git_diff_tree_options " . join(' ', @ARGV));
	}
}
sub
_push
{
	my $url = generate_url(shift);
	gitcmd('rsync', '-qL', "$url/HEAD", '.git/REMOTE_HEAD');
	chomp (my $remote_head = _read_file('.git/REMOTE_HEAD'));
	my $head        = head() || die("No local HEAD\n");
	if ($head eq $remote_head) {
		print STDERR "Nothing to push.\n";
		return;
	}
	print "head => $head\nremote => $remote_head\n";
	my @revlist     = gitcmdout('git-rev-list', $head);
	if (! grep(/^${remote_head}$/, @revlist)) {
		print STDERR "Remote is ahead or unrelated: Need to pull first?\n";
		exit 1;
	}
	gitcmd('rsync', '-qa', '--ignore-existing', ".git/objects/.", "$url/objects/.");
	gitcmd('rsync', '-q', '.git/HEAD', "$url/HEAD");
}
sub
clone
{
	my %options;
	GetOptions(\%options, "l");
	my $url     = shift(@ARGV);
	my $project = shift(@ARGV);
	if (! defined($url) || ! defined($project)) {
		die("Usage: $0 clone <url> <project>\n");
	}
	$url = generate_url($url);
	if (defined($options{l})) {
		if (-d $url) {
			-d ".git"              && die("$0 clone $project: Don\'t create repository in a repository.");
			mkdir($project, 0755)  || die("$0 clone $project: mkdir: $!");
			chdir($project)        || die("$0 clone $project: chdir: $!");
			mkdir('.git', 0755)    || die("$0 clone $project: mkdir: $!");
			symlink("$url/objects", '.git/objects') || die("Can\'t symlink object database");
		} else {
			die("Can't symlink from network repositories\n");
		}
	} else {
		setup($project);
	}
	parent($url);
	pull();
}
sub
parent
{
	my $parent = $_[0];
	if (defined($parent)) {
		if ($parent eq "-c") {
			if ( -f ".git/PARENT" || -l ".git/PARENT") {
				unlink(".git/PARENT") || die("can't delete .git/PARENT: $!");
			}
			print STDERR "$0 parent: Parent removed\n";
		} else {
			$parent = generate_url($parent);
			_write_file('.git/PARENT', "$parent\n");
		}
	} else {
		if ( -f ".git/PARENT" || -l ".git/PARENT") {
			
			chomp($parent = _read_file('.git/PARENT'));
			print "${parent}\n";
		} else {
			print STDERR "$0 parent: No parent specified\n";
		}
	}
}
sub
diff
{
	my %options;
	local @ARGV = @_;
	GetOptions(\%options, 'r=s', 'f', 'c', 'v', "S=s", 'root');
	@_ = @ARGV;
	my $dir = tempdir(CLEANUP => 1);
	refresh();
	my $git_diff_tree_options = '';
	if (defined ($options{'S'})) {
		$git_diff_tree_options .= " -S'$options{'S'}'";
	}
	if (defined ($options{'root'})) {
		$git_diff_tree_options .= " --root";
	}
	if (defined($options{'r'})) {
		if($options{'r'} =~ /:/) {
			my ($first, $second) = split(/:/, $options{'r'});
			system("git-diff-tree -p -r $first $second $git_diff_tree_options " . join(' ', @_));
		} else {
			if (defined ($options{'v'})) {
				$git_diff_tree_options .= ' -v';
			}
			system("git-diff-tree -p -r $options{'r'} $git_diff_tree_options " . join(' ', @_));
		}
	} elsif (defined($options{"f"})) {
		system("git-diff-files -r -z $git_diff_tree_options " . join(' ', @_) . " | git-diff-helper -z");
	} elsif (defined($options{"c"})) {
		system("git-diff-cache --cached -r -z HEAD $git_diff_tree_options " . join(' ', @_) . " | git-diff-helper -z");
	} else {
		system("git-diff-cache -r -z HEAD $git_diff_tree_options " . join(' ', @_) . " | git-diff-helper -z");
	}
}
sub
treediff
{
	my %options;
	local @ARGV = @_;
	Getopt::Long::Configure("pass_through");
	GetOptions(\%options, 'p=s');
	Getopt::Long::Configure("no_pass_through");
	@_ = @ARGV;
	pull('-o', '-l', $options{'p'});
	diff('-r', 'REMOTE_HEAD:HEAD', @_);
}
sub
init_db
{
	gitcmd( 'init-db', @_ );
}
sub
setup
{
	my $project = shift    || die ("usage: $0 setup project");
	-d ".git"              && die ("$0 setup $project: Don\'t create repository in a repository.");
	mkdir($project, 0755)  || die ("$0 setup $project: mkdir: $!");
	chdir($project)        || die ("$0 setup $project: chdir: $!");
	gitcmd('git-init-db');
}
sub
revert
{
	# TODO handle revert of add/remove --tg 02:32 05-05-06
	my @in    = ();
	my $fh;
	if (! defined($_[0])) {
		return;
	}
	if ($_[0] eq "-") {
		@in = <STDIN>;
	} else {
		@in = @_;
	}
	my $head = head();
	($fh, $ENV{GIT_INDEX_FILE}) = tempfile(CLEANUP => 1);
	gitcmd('git-read-tree', $head);
	foreach (@in) {
		chomp($_);
		$_ =~ s#^\.\/##;
		if (/^\./) {
			print STDERR "Warning: Skipping $_\n";
			next;
		}
		if (-d $_) {
			next;
		}
		gitcmd('git-checkout-cache', '-f', $_);
	}
	delete($ENV{GIT_INDEX_FILE});
	foreach (@in) {
		chomp($_);
		$_ =~ s#^\.\/##;
		if (/^\./) {
			next;
		}
		if (! -f $_) {
			next;
		}
		gitcmd('git-update-cache',  $_);
	}
}
sub
add
{
	my @in    = ();
	if (defined($_[0]) && $_[0] eq "-") {
		@in = <STDIN>;
	} else {
		@in = @_;
	}
	foreach (@in) {
		chomp($_);
		$_ =~ s#^\.\/##;
		if (/^\./) {
			print STDERR "Warning: Skipping $_\n";
			next;
		}
		if (-d $_) {
			next;
		}
		if (! -f $_) {
			print STDERR "Warning: Skipping nonexisting file: $_\n";
			next;
		}
		gitcmd( 'git-update-cache', '--add', '--', $_ );
	}
}
sub
ci
{
	my @in    = ();
	if (defined($_[0]) && $_[0] eq "-") {
		@in = <STDIN>;
	} else {
		@in = @_;
	}
	foreach (@in) {
		chomp($_);
		$_ =~ s#^\.\/##;
		if (/^\./) {
			print STDERR "Warning: Skipping $_\n";
			next;
		}
		if (-d $_) {
			next;
		}
		if (! -f $_) {
			print STDERR "Warning: Skipping nonexisting file: $_\n";
			next;
		}
		gitcmd( 'git-update-cache', '--', $_ );
	}
}
sub
rm
{
	my @in    = ();
	if (defined($_[0]) && $_[0] eq "-") {
		@in = <STDIN>;
	} else {
		@in = @_;
	}
	foreach (@in) {
		chomp($_);
		$_ =~ s#^\.\/##;
		if (/^\./) {
			print STDERR "Warning: Skipping $_\n";
			next;
		}
		if (-d $_) {
			next;
		}
		if (-f $_) {
			unlink($_) || die ("$0 rm $_: $!");
		}
		gitcmd( 'git-update-cache', '--remove', '--' , $_ );
	}
}
sub
head
{
	my $head = $_[0];
	if (defined($head)) {
		if ($head eq "") {
			if ( -f ".git/HEAD" || -l ".git/HEAD") {
				unlink(".git/HEAD") || die "failed to delete .git/HEAD: $!\n";
			}
		} else {
			chomp($head);
			_write_file( '.git/HEAD', "$head\n" );
		}
	} else {
		if (-f '.git/HEAD') {
			chomp( $head = _read_file( '.git/HEAD' ) );
		} else {
			$head = undef;
		}
	}
	return $head;
}
sub
print_commit
{
	my $commit = shift || die("need commit");
	print "commit $commit\n";
	foreach (gitcmdout('git-cat-file', 'commit', $commit)) {
		if (/^(author|committer)(.+>\s)(\d+)\s([+-]?\d{4})$/) {
			# local $ENV{TZ} = $4;
			print "$1$2" . localtime($3) . "\n";
		} else {
			print "$_\n";
		}
	}
	print "\n";
}
sub
commit
{
	my $head = head();
	chomp (my $tree = gitcmdout('git-write-tree'));
	if (dirty_files()) {
		die "$0 commit: Get rid of dirty files first.\n";
	}
	if (defined($head)) {
		my %hash = commit_hash($head);
		if ($hash{tree} eq $tree) {
			die "$0 commit: The commit wouldn't commit anything different.\n";
		}
		$head = gitcmdout('git-commit-tree', $tree, '-p', $head);
	} else {
		$head = gitcmdout('git-commit-tree', $tree);
	}
	head($head);
}
sub
log
{
	my $head = head() || return ;
	my $pid;
	if ( ! -p STDIN && ! -p STDOUT ) {
		my ( $r, $w );
		pipe( $r, $w ) || die "Failed to pipe: $!";
		defined( $pid = fork ) || die "Failed to fork: $!";
		if ( $pid ) { # Parent
			$SIG{INT} = $SIG{TERM} = $SIG{HUP} = sub {
				kill 15, $pid;
				exit 1;
			};
			close $r;
			close STDOUT;
			open STDOUT, '>&', $w || die "Failed to redirect STDOUT: $!";
		} else { # pager child
			close $w;
			close STDIN;
			open STDIN, '<&', $r || die "Failed to redirect STDIN: $!";
			if ( $ENV{PAGER} ) {
				exec( $ENV{PAGER} );
			} else {
				exec( 'less', '-r', '-' );
			}
		}
	}
	foreach (gitcmdout('git-rev-list', $head)) {
		print_commit($_);
	}
	if ( $pid ) {
		close STDOUT;
		waitpid($pid, 0);
	}
}
sub
commit_hash
{
	my %hash    = ();
	my $id      = shift || die ("$0: commit_hash: expect one argument");
	my $comment = 0;
 	my @lines = gitcmdout('git-cat-file', 'commit', $id);
	foreach (@lines) {
		chomp;
		if ($comment) {
			push(@{$hash{comment}}, $_);
			next;
		}
		if (/^tree\s(\w{40})$/) {
			$hash{tree} = $1;
			next;
		}
		if (/^parent\s(\w{40})$/) {
			push(@{$hash{parent}}, $1);
			next;
		}
		if (/^author\s(.*)\s<(.*)>\s(.*)$/) {
			$hash{author_name}  = $1;
			$hash{author_eMail} = $2;
			$hash{author_date}  = $3;
			next;
		}
		if (/^committer\s(.*)\s<(.*)>\s(.*)$/) {
			$hash{committer_name}  = $1;
			$hash{committer_eMail} = $2;
			$hash{committer_date}  = $3;
			next;
		}
		if (/^$/) {
			$comment = 1;
		}
	}
	return(%hash);
}
sub
first_parent
{
	my $this = shift || die ("$0: parent called without object");
	my %hash = commit_hash($this);
	if (! defined(@{$hash{parent}}[0])) {
		return "";
	}
	return(@{$hash{parent}}[0]);
}
sub
undo
{
	my $head = head();
	if (! defined($head)) {
		return;
	}
	if (    dirty_files()
            || `git-diff-cache -r --cached HEAD`) {
		print STDERR "Get rid of dirty files / uncommited deltas first.\n";
		exit 1;
	}
	my $newhead = first_parent($head);
	print "oldhead $head\nnewhead $newhead\n";
	head($newhead);
	checkout('-f');
}
sub
object_type
{
	my $id = shift;
	defined $id && $id =~ /^[A-Za-z0-9]{40}$/ ||
		die "Invalid sha1 id '$id'";
	my $type = gitcmdout('git-cat-file', '-t', $id );
	chomp $type;
	return $type;
}
sub
tree_id
{
	my $id = shift;
	( $id ) = grep { defined }
		map { /^tree ([A-Za-z0-9]{40})$/ ? $1 : undef }
			gitcmdout( 'git-cat-file', 'commit', $id ) or
		die "Unable to find tree id for commit id $id";
	object_type( $id ) eq 'tree' ||
		die "tree id from commit is not a tree object!";
	return $id;
}
sub
parent_id
{
	my $id = shift;
	my ( $pid ) = grep { defined }
		map { /^parent ([A-Za-z0-9]{40})$/ ? $1 : undef }
			gitcmdout( 'git-cat-file', 'commit', $id ) or
		die "Unable to determine parent commit of commit $id";
	return $pid;
}

# int # main(int argc, char **argv) # {

if (defined($ARGV[0]) && defined($commands{$ARGV[0]})) {
	my $string = shift;
	if ( ! -d ".git"
	&&   ! ($string eq "setup"
             || $string eq "init-db"
             || $string eq "clone")) {
		die("$0 $string: Not in a git BASE directory");
	}
	$commands{$string}->(@ARGV);
} else {
	if (defined($ARGV[0])) {
		print STDERR "No such command: $ARGV[0]\n\n";
	}	
	usage();
}
# }
{
	my %gitcmd;
	sub
	gitcmdpath
	{
		my $cmd = shift;
		unless ( defined $gitcmd{$cmd} ) {
			local $/ = "\n";
			chomp( $gitcmd{$cmd} = `which $cmd` );
			return undef if $gitcmd{$cmd} eq '';
		}
		return $gitcmd{$cmd};
	}
	sub
	gitcmd
	{
		my $cmd = shift;
		gitcmdpath( $cmd ) || die "command '$cmd' not found";
		my $r = system( $gitcmd{$cmd}, @_ );
		die "$cmd failed: " . _gitcmderrmsg( $cmd )
			if $r != 0;
		return 1;
	}
	sub
	gitcmdinout
	{
		my $infh = shift;
		my $cmd  = shift;
		gitcmdpath( $cmd ) || die "command '$cmd' not found";
		my ( $r, $w );
		pipe( $r, $w ) || die "Failed to pipe: $!";
		my $pid = fork();
		die "Failed to fork: $!" unless defined $pid;
		if ( $pid ) {
			close $w;
			local $/;
			local $_ = <$r>;
			close $r;
			my $kid = waitpid( $pid, 0 );
			die "Hmm, auto reaping in place?" if $kid == -1;
			die "$cmd failed: " . _gitcmderrmsg( $cmd )
				if $? & 127 || $? >> 8 != 0;
			if ( wantarray ) {
				return split( "\n", $_ );
			} else {
				return $_;
			}
		} else {
			close $r;
			close STDOUT;
			close STDIN;
			open STDIN, '<&', $infh || die "Failed to rediret STDIN";
			open STDOUT, '>&', $w || die "Failed to redirect STDOUT";
			exec( $gitcmd{$cmd}, @_ );
		}
	}
	sub
	gitcmdout
	{
		my $cmd = shift;
		gitcmdpath( $cmd ) || die "command '$cmd' not found";
		my ( $r, $w );
		pipe( $r, $w ) || die "Failed to pipe: $!";
		my $pid = fork();
		die "Failed to fork: $!" unless defined $pid;
		if ( $pid ) {
			close $w;
			local $/;
			my $ret = <$r>;
			close $r;
			my $kid = waitpid( $pid, 0 );
			die "Hmm, auto reaping in place?" if $kid == -1;
			die "$cmd failed: " . _gitcmderrmsg( $cmd )
				if $? & 127 || $? >> 8 != 0;
			if (wantarray) {
				return split("\n", $ret);
			} else {
				return $ret;
			}
		} else {
			close $r;
			close STDOUT;
			open STDOUT, '>&', $w || die "Failed to redirect STDOUT";
			exec( $gitcmd{$cmd}, @_ );
		}
	}
	sub
	_gitcmderrmsg
	{
		my $cmd = shift;
		my $e;
		if ( $? == -1 ) {
			$e = "failed to execute $gitcmd{$cmd}: $!";
		} elsif ( $? & 127 ) {
			$e = sprintf( 'child die from signal %d', ( $? & 127 ) );
			$e .= ' (with coredump)' if $? & 128;
		} else {
			$e = sprintf( 'child exit value: %d', $? >> 8 );
		}
		return $e;
	}
}
sub
_recur_mkdir
{
	my $dir = shift;
	my @dir = split( /\//, $dir );
	my $path = '';
	while ( @dir ) {
		$path .= '/' . shift @dir;
		( -d $path ) || mkdir( $path ) ||
			die "Failed to mkdir $path: $!";
	}
}
sub
_read_file
{
	my $file = shift;
	my $fh;
	open $fh, '<', $file || die "failed to read $file: $!\n";
	if ( wantarray ) {
		my @r = <$fh>;
		close $fh || die "failed to close $file: $!\n";
		return @r;
	} else {
		local $/;
		my $r = <$fh>;
		close $fh || die "failed to close $file: $!\n";
		return $r;
	}
}
sub
_write_file
{
	my $file = shift;
	my $fh;
	open $fh, '>', $file || die "failed to write $file: $!\n";
	if ( @_ ) {
		print $fh join( $/, @_ );
	}
	close $fh            || die "failed to close $file: $!\n";
	return 1;
}
# vim:set noexpandtab:
Thomas Glanzmann· May 29, 2005, 23:10 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Hello, one obvious fix:

> 			$message .= "<= ${current_url} (nothing to merge)\n";
> 			$message   .= "<= ${current_url} (bringing head ahead)\n";

the parents of such 'merges' should *not* be referenced in the commit object E.g. this is wrong: 'git-commit-tree -p nothing_to_merge -p just_bringing_head_ahead'. Leaving the message in the log section of the commit object could be useful, maybe. Thoughts?

And I should doublecheck if more than 16 parents show up in git-commit-tree when doing the commits and throw an error.

	Thomas
Thomas Glanzmann· May 29, 2005, 23:16 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Hello,
> > 			$message .= "<= ${current_url} (nothing to merge)\n";
> > 			$message   .= "<= ${current_url} (bringing head ahead)\n";
> the parents of such 'merges' should *not* be referenced in the commit
> object E.g. this is wrong: 'git-commit-tree -p nothing_to_merge -p
> just_bringing_head_ahead'. Leaving the message in the log section of the
> commit object could be useful, maybe. Thoughts?

Wrong, wrong, wrong. If 'head ahead' condition shows up, I have to kick the previous head out (not the current), don't I?

	Thomas
Thomas Glanzmann· May 29, 2005, 23:46 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Hello, yet another bug. If there is only one head left after the for loop this head is *set* not *commitet* as new HEAD because it comes from a 'pull into empty tree' or from a 'just head ahead'-condition.

	Thomas
Thomas Glanzmann· May 29, 2005, 23:56 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Hello, here is the actual version of my merge function. However I have a question left over: When I do an automatic merge or a threeway merge should I set the head for the next round to the remote one or just let it be the current one (this is the current behaviour) or maybe make it configurable? Note: At the moment the *local* head for the merge-base is only modified during the set of the first head *or* on a head forward condition. I have no clue. I am going to write the 'more on merging chapter' after I have figured this out. ;-)

sub merge {

	my $message      = undef;
	my $head         = undef;
	my $last_tree    = undef;
	my $fh;
	my @heads        = ();
	foreach my $r (@_) {
		my $current_head = @{$r}[0];
		my $current_url  = @{$r}[1];
		print "current_head => $current_head\ncurrent_url => $current_url\n";
		push(@heads, '-p', ${current_head});
		if (! defined($last_tree)) {
			$message    = "=> ${current_url}\n";
			$head       = $current_head;
			$last_tree  = $current_head;
			
			if (@_ == 1) {
				head($head);
				return;
			}
			next;
		}
		my $merge_base = gitcmdout('git-merge-base', $head, $current_head)
				 || die ("no merge-base");
		chomp($merge_base);
		print "head => $head\nremote => $current_head\nbase => $merge_base\n";
	
		if ($merge_base eq $current_head) {
			$message .= "<= ${current_url} (nothing to merge)\n";
			$#heads -= 2;
			next;
		}
		if ($merge_base eq $head) {
			$message   .= "<= ${current_url} (bringing head ahead)\n";
			$head       = ${current_head};
			$last_tree  = ${current_head};
			$#heads -= 4;
			push(@heads, '-p', $current_head);
			next;
		}
		gitcmd('git-read-tree', '-m', $merge_base, $last_tree, $current_head);
		if (! defined($last_tree = write_tree())) {
			system('git-merge-cache', '-o', 'git-merge-one-file-script', '-a');
			if (! defined($last_tree = write_tree())) {
				# FIXME: Make manual intervention possible
				# --tg 23:11 05-05-29
				die("Couldn't merge automatically: Call 'git resolve'");
			}
			$message .= "<= ${current_url} (threeway merge)\n";
		} else {
			$message .= "<= ${current_url} (automatic merge)\n";
		}
	}
	if (@heads == 1) {
		head(@head[0]);
		return;
	}
	open($fh, "+>", undef);
	print $fh $message;
	seek($fh, 0, 0);
	$head = gitcmdinout($fh, 'git-commit-tree', $last_tree, @heads);
	chomp($head);
	close $fh;
	head($head);
	return;
}
	Thomas
Junio C Hamano· May 30, 2005, 00:50 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Instead of inflicting a Perl script on us, maybe writing a textual specification of what you want it to do would help to clarify your thinking and help us understand the problem you are trying to describe a lot better. I think Linus publicly stated he does not do Perl much. I am OK with Perl but I'd rather answer questions posed in a more reader-friendly manner, rather than having to guess what the caller is expected to give this "merge" sub, which you do not document well.

I think I've already asked you something quite similar when you posted another part of your script for parsing the new diff-raw format, which I responded with something like: "Without knowing how this sub is supposed to be called, I think you are stripping leading colon from a filename if there is one". Anyhow.

Are you trying to implement an Octopus capable N-way merger? If so, the way I would do would be something like this:

 - Accept N parameters, which are heads being merged.
 - Sanity check that given heads are commits, and N <= 16.
 - Initialize a set, HTM (heads to be merged), to contain all of
   the supplied heads.
 - Remove one commit from HTM, call it H0.
 - Initialize a variable, BASE, with H0.  This variable
   determines the base of the merge in the commit topology.
 - Initialize a variable, T, with tree associated with H0.  This
   variable holds the "current intermediate merge result" tree.
 - While HTM is not empty, loop over the following:
   - Remove one commit out of HTM; call it H1.
   - MB = git-merge-base BASE H1;
   - If MB is either BASE or H1, then you have a fast forward.
     Take either BASE or H1 that is not MB and update variable
     BASE with it, and update variable T with the tree
     associated with it.  Continue with the loop (i.e. Perl
     "next").
   - Run your usual read-tree -m MB T H1 and git-merge-cache; as
     Linus explained, if this step ends up involving any
     non-trivial merges, you should not do an Octopus.  So in
     such a case, if HTM is not empty yet, barf (i.e. Perl
     "die", or at least "last").
   - Do not touch your ${GIT-.git}/HEAD in any way at this
     moment.
   - Update variable T with git-write-tree of the resolved cache
     contents.
   - Update varaible BASE with MB.
   - Continue with the loop. 
 - We exited the loop by now.  HTM being empty means that T has
   the result of N-way merge.  Create a single commit object
   that has all the commits you have merged as its parents, and
   register T as its associated tree.  I would imagine recording
   that commit in ${GIT-.git}/HEAD is what the user usually
   wants but there may be use cases that it may not be
   appropriate (I do not do Porcelain so I do not know).
Junio C Hamano· May 30, 2005, 00:57 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

>>>>> "JCH" == Junio C Hamano <junkio@cox.net> writes:

JCH> - If MB is either BASE or H1, then you have a fast forward. JCH> Take either BASE or H1 that is not MB and update variable JCH> BASE with it, and update variable T with the tree JCH> associated with it. Continue with the loop (i.e. Perl JCH> "next").

Chuck this part please.  I was not thinking.
Thomas Glanzmann· May 30, 2005, 01:33 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

Hello,
Show 5 quoted lines
> JCH>    - If MB is either BASE or H1, then you have a fast forward.
> JCH>      Take either BASE or H1 that is not MB and update variable
> JCH>      BASE with it, and update variable T with the tree
> JCH>      associated with it.  Continue with the loop (i.e. Perl
> JCH>      "next").
> Chuck this part please.  I was not thinking.

No, I don't because I think this exactly what I have to do here. And yes, there can be fast forwards. :-)

But note: On a fast forward condition we have to remove a element from
COMMIT_HEADS: the (last) or (last - 1). Depending on if 'local' ==
MERGE_BASE or 'remote' == MERGE_BASE.
	Thomas
Thomas Glanzmann· May 30, 2005, 01:30 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

Hello, okay let me try again.

I have a function merge which gets a sorted array of heads. Heads can be unlimited at the time because some of the heads can be included into other heads (they're a subset) and so they don't show up in the commit object. I call this array MERGE_HEADS.

Note: If I pull into an empty tree (no HEAD) there is only one head in
this array which corresponds to the remote_head. Otherwise the first
element is *always* the local HEAD.

After that I am starting looping over MERGE_HEADS. The first thing I have to do is getting the first element out of this array and safe it for later reference I call this 'head'. Also I have to push this head in a another array called COMMIT_HEADS which will be used to create the final commit object later on. The latter will be done for every loop pass. next;

Note: If I left the the loop because there are no more MERGE_HEADS to
work on and my COMMIT_HEADS array consists only of *one* member I don't
create a COMMIT object, but save it as new HEAD because we're in a fast
forward condition (this could be pulling into an empty tree; having many
fast forward object (remote is ahead or included into the current
'head'). On the contrary if I have *more* than one object I call
commit-tree with the COMMIT_HEADS as arguments and save the new head
return from this call.

Now I start processing the second HEAD from MERGE_HEADS. I use merge_base to find out the MERGE_BASE. If this MERGE_BASE == head than we have a (remote is fast forward condition) so our CURRENT_HEAD becomes head and I delete the week of the last element of COMMIT_HEADS (but leaving the CURRENT_HEAD in COMMIT_HEADS). next; If MERGE_BASE == CURRENT_HEAD than CURRENT_HEAD is already included in our history so no need to anything, but get it out of COMMIT_HEADS. next; If it isn't a fast forward or already included case, we do automatic/threeway/manual merge and save the resulting tree for the maybe to come next automatic/threeway/manual merge. And of course also leaving the CURRENT_HEAD in COMMIT_HEADS. FIXME: Do we need to update our 'head' to the REMOTE_HEAD? next;

Oh and of course the sanity check: I can't commit-tree more than 16 parents at a time. (16 is of course the define mentioned by Linus before).

That's it.
	Thomas
Junio C Hamano· May 30, 2005, 07:57 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

>>>>> "TG" == Thomas Glanzmann <sithglan@stud.uni-erlangen.de> writes:

TG> Note: If I pull into an empty tree (no HEAD) there is only one head in TG> this array which corresponds to the remote_head. Otherwise the first TG> element is *always* the local HEAD.

"An empty tree (no HEAD)"? Is your definition of "an empty tree" the same as "empty" directory after you do "mkdir empty && cd empty && git-init-db", followed by bunch of git-*-pull to get the objects and commits from other reposititories being involved in the merge but without touching .git/HEAD? If so, why cannot I do the git-*-pull from multiple repositories and merge them together? Why "there is only one head in this array that is remote_head"? Oh, I guess I am missing your definition of "remote_head". Puzzled...

Anyhow I presume that if your ${GIT-.git}/HEAD exists, you include it as the first element of MERGE_HEADS.

TG> I have a function merge which gets a sorted array of heads. Heads can be TG> unlimited at the time because some of the heads can be included into TG> other heads (they're a subset) and so they don't show up in the commit TG> object. I call this array MERGE_HEADS.

Sorry I am not very good at this "thinking" thing, and I need to draw pictures. Please bear with me.

    (line of dev C)-------------C    We are here, trying to merge
    (line of dev B)---(merge)---B    these three lines of devs:
    (line of dev A)---A/             A, B and C
    MERGE_HEADS = (A B C)
    A is actually a "subset" of B

Is this what you mean by "subset"? Are these "subset" HEAD the only thing that causes fast forwards?

My gut feeling without thinking much is that it might be easier to first cull such fast forward heads by using N-way rev-tree before you do anything else. If only one head survives after that, then that head would be your new head and you do not have to go through any merges. Otherwise you merge those independent heads without worrying about fast forwards. How does that sound?

Thomas Glanzmann· May 30, 2005, 08:36 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

Hello,

[ => Skip till next ']' if you want because my approach doesn't work out for all cases in the optimal way.

Show 5 quoted lines
> "An empty tree (no HEAD)"?  Is your definition of "an empty
> tree" the same as "empty" directory after you do "mkdir empty &&
> cd empty && git-init-db", followed by bunch of git-*-pull to get
> the objects and commits from other reposititories being involved
> in the merge but without touching .git/HEAD?

Yep. In practice it isn't anything else than just using the first 'remote' HEAD as 'local' HEAD if there is no 'local' HEAD. But for merge this doesn't really matter.

> If so, why cannot I do the git-*-pull from multiple repositories and
> merge them together?  Why "there is only one head in this array that
> is remote_head"?  Oh, I guess I am missing your definition of
> "remote_head".  Puzzled...

My intention by writing this note was to clear things up. That it also works for cases where you do the 'initial clone' for one and multiple remotes.

> Anyhow I presume that if your ${GIT-.git}/HEAD exists, you
> include it as the first element of MERGE_HEADS.
Affirmative.
> TG> I have a function merge which gets a sorted array of heads. Heads can be
> TG> unlimited at the time because some of the heads can be included into
> TG> other heads (they're a subset) and so they don't show up in the commit
> TG> object. I call this array MERGE_HEADS.
Here is a typo: MERGE_HEADS is *not* sorted. But it is 'ordered'.
> Sorry I am not very good at this "thinking" thing, and I need to
> draw pictures.  Please bear with me.
Of course, I do.
>     (line of dev C)-------------C    We are here, trying to merge
>     (line of dev B)---(merge)---B    these three lines of devs:
>     (line of dev A)---A/             A, B and C
>     MERGE_HEADS = (A B C)
>     A is actually a "subset" of B
> Is this what you mean by "subset"?  Are these "subset" HEAD the
> only thing that causes fast forwards?

Exactly, but it has not to be a merge it also can be a linear development. What matters that A is referenced in any way in the history of B.

In this case where A is referenced in the history of B, we just discard HEAD A, because it is already merged. So A will never show up in git-commit-tree in one of its "-p" options. But I mention it in the commit-text as 'there was nothing todo'.

If we take now your example and switch the order A and B we have
MERGE_HEADS = (B A C) and A is still in the history of B:

We do exactly the same here only that in the above scenario we have to kick out the previous COMMIT_HEAD when processing B while we drop the current COMMIT_HEAD when processing A in this scenario. So to clear things up:

The HEAD we are working on is surrouned by '*'s.

MERGE_HEADS = (*B* A C) => COMMIT_HEADS = (B) MERGE_HEADS = (B *A* C) => COMMIT_HEADS = (B) /* note: A never did it in COMMIT_HEADS because it was referenced in history of B */ MERGE_HEADS = (B A *C*) => COMMIT_HEADS = (B C)

while in the above example with your initial order it is:

MERGE_HEADS = (*A* B C) => COMMIT_HEADS = (A) MERGE_HEADS = (A *B* C) => COMMIT_HEADS = (B) /* note: A is kicked out of COMMIT_HEADS ... see above */ MERGE_HEADS = (A B *C*) => COMMIT_HEADS = (B C)

]
Show 7 quoted lines
> My gut feeling without thinking much is that it might be easier
> to first cull such fast forward heads by using N-way rev-tree
> before you do anything else.  If only one head survives after
> that, then that head would be your new head and you do not have
> to go through any merges.  Otherwise you merge those independent
> heads without worrying about fast forwards.  How does that
> sound?

You're right. Because this would work out bad (unneccessary automatic/threeway/manual merge) if we twist MERGE_HEADS again:

MERGE_HEADS = (*C* A B) => COMMIT_HEADS (C) MERGE_HEADS = (C *A* B) => COMMIT_HEADS (C A) /* A stays in because it is not in the history of C; -> unneccessary merge */ MERGE_HEADS = (C A *B*) => COMMIT_HEADS (C A B)

Okay, so I have to elminate all fast forward conditions in the first place? How do I do this:

foreach CURRENT_HEAD (@MERGE_HEADS) {
	foreach COMPARE_HEAD (@MERGE_HEADS) {
		if (COMPARE_HEAD != CURRENT_HEAD
		&&  COMPARE_HEAD is_included_into_history_of CURRENT_HEAD) {
			@WIPE_HEADS += COMPARE_HEAD;
		}
	}
}
foreach (@WIPE_HEADS) {
	grep -v @WIPE_HEADS @MERGE_HEADS;
}
	Thomas
Thomas Glanzmann· May 30, 2005, 09:21 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

Hello, I also have to strip duplicate HEADs out. So we do the following:

run 0: kill dups
run 1: kill HEADs which are referenced in the history of other HEADs
run 2: do the merging (still don't know to what I should set the local
       HEAD to the 'left' or 'right' part. Maybe we should create temporary
       commit object so that 'merge-base' can better work on them?
	Thomas
Junio C Hamano· May 30, 2005, 10:20 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

>>>>> "TG" == Thomas Glanzmann <sithglan@stud.uni-erlangen.de> writes:

TG> Hello, TG> I also have to strip duplicate HEADs out. So we do the following:

TG> run 0: kill dups TG> run 1: kill HEADs which are referenced in the history of other HEADs

I think merge-base between A and A is always A, so the second pass would eliminate the dups anyway...

Thomas Glanzmann· May 30, 2005, 12:11 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

Hello,
> I think merge-base between A and A is always A, so the second pass
> would eliminate the dups anyway...

two scenarios come into my mind which could clash (second ommited because it would rely that the 'local' HEAD is changes on threeway merge)

consider the follow MERGE_HEADS = (A B B)
    /----- A
   /
-GCA------ B

Now B gets twice automatic/threeway/manual merged into A which is just stupid and couldn't work out.

------------------------------------------------------------------------------
But I still don't know how to handle the following scenario:
    /----- A
   /
-GCA------GCA---- B
            \---- C

MERGES_HEAD = (A B C). I think the best way would be introduce a temporary commit object otherwise C into AB merge would have merge_base on the first GCA which is suboptimal and maybe wrong isn't it?

    /----- A -------D---E
   /               /   /
-GCA------GCA---- B   /
            \------- C

Where D is a temporary COMMIT obeject to use the second GCA to merge C with D and gets E.

Gruesse,
	Thomas
Junio C Hamano· May 30, 2005, 17:54 UTC · re: Thomas Glanzmann · lore

Re: More gitweb queries..

>>>>> "TG" == Thomas Glanzmann <sithglan@stud.uni-erlangen.de> writes:

Disregard my last MB(A,A)===A thing. I really was not thinking, but in N-way merge I think rev-tree is your friend. Unlike merge-base which always does two heads at a time, rev-tree works N-way. You should be able to give many heads to it and have it help you figure out the ancestory relationship across them.

TG> But I still don't know how to handle the following scenario:

TG> /----- A TG> / TG> -GCA#1---GCA#2--- B TG> \---- C

TG> MERGES_HEAD = (A B C). I think the best way would be introduce a TG> temporary commit object otherwise C into AB merge would have merge_base TG> on the first GCA which is suboptimal and maybe wrong isn't it?

I think you can run the same algorithm for those pairwise ancestors and favor the less common ones. In the above drawing, you would first find out the three common pair-wise ancestors.

In your drawing,
    GCA(A,B) = GCA#1
    GCA(B,C) = GCA#2
    GCA(C,A) = GCA#1
    GCA(GCA#1,GCA#2) = GCA#1

Then you notice that GCA#2 is less common than GCA#1 (indeed GCA#1 is contained in GCA#2). So favoring the GCA#2, you do the merge pair that uses it as the merge base, namely B and C, first.

You scan the pairwise list again and find the pair that have not merged and uses the least common GCA as merge base and keep going. For the definition of less common, in many cases those GCA#n would not be related at all and cannot be compared by topology only, so you would probably need to come up with a heuristics to break such ties. Off the top of my head, you could compare commit timestamps (prefer younger), sum of commit chain length from GCA#n to its two head pairs (prefer shorter), sum of number of changed files from GCA#n to its two head pairs (prefer smaller).

TG> /----- A -------D---E TG> / / / TG> -GCA------GCA---- B / TG> \------- C

TG> Where D is a temporary COMMIT obeject to use the second GCA to merge C TG> with D and gets E.

Yes, if you ended up merging A and B first you would have something like that. I think you should not do that in the first place, and try to merge the "most recently diverged" pair first, like this:

         /----- A -----------Y
        /                   / 
     -GCA#1---GCA#2--- B --X
                 \---- C -/

If we are still talking about Octopus (Tripus in this case), you would not want to actually make a commit X. When you merge B and C, you consider that such a resulting tree resides at GCA#2 (merge base of B and C) for the purposes of further merge computation. Then you merge that resulting tree with A, using GCA#1.

Thomas Glanzmann· May 27, 2005, 19:31 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

Hello,
>  - looking around, the ALSA guys aren't the only ones that start off with 
>    an empty line, so it's probably worth fixing the summary etc to ignore 
>    whitespace at the beginning rather than give empty summary reasons.

for me the formatting of an empty commit comment screw up the layout. I don't know if it already is fixed, if not, I will soon report a patch.

	Thomas
Junio C Hamano· May 27, 2005, 19:32 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

>>>>> "LT" == Linus Torvalds <torvalds@osdl.org> writes:

LT> Combining some of the features of the two (that über-cool revision LT> history graph from gitk rules, for example) might be cool. I get the LT> urge to do octopus-merges in the kernel just because of how good they LT> look in gitk ;) ]

Hey, Octopus is what you explicitly told me not to do ;-).
Linus Torvalds· May 27, 2005, 19:48 UTC · re: Junio C Hamano · lore

Re: More gitweb queries..

On Fri, 27 May 2005, Junio C Hamano wrote:
Show 9 quoted lines
>
> >>>>> "LT" == Linus Torvalds <torvalds@osdl.org> writes:
> 
> LT>   Combining some of the features of the two (that über-cool revision 
> LT>   history graph from gitk rules, for example) might be cool. I get the 
> LT>   urge to do octopus-merges in the kernel just because of how good they
> LT>   look in gitk ;) ]
> 
> Hey, Octopus is what you explicitly told me not to do ;-).
I know, I know. I said "I get urges", I didn't say I'll do it.
I'll try to control myself.
Maybe.
		Linus
Benjamin Herrenschmidt· May 27, 2005, 23:12 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

Show 5 quoted lines
>   On that small note, I also find "gitk" very cool indeed, too bad about 
>   the fact that tk/tcl seems to always end up looking so _ugly_. Is there 
>   any way to get anti-aliased fonts and a less 'Motify' blocky look from 
>   tcl/tk? Every time I see that, I feel like I'm back in the last century  
>   or something.
Heh, tell paulus, he loves Tk :-)

He told me the next version of Tk will have anti aliased fonts. I don't know about blockyness of the widgets tho.

>   Combining some of the features of the two (that über-cool revision 
>   history graph from gitk rules, for example) might be cool. I get the 
>   urge to do octopus-merges in the kernel just because of how good they
>   look in gitk ;) ]
Kay Sievers· May 27, 2005, 23:59 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

On Fri, May 27, 2005 at 12:24:20PM -0700, Linus Torvalds wrote:
>  - looking around, the ALSA guys aren't the only ones that start off with 
>    an empty line, so it's probably worth fixing the summary etc to ignore 
>    whitespace at the beginning rather than give empty summary reasons.

That is already fixed a few days ago, but unfortunately only in my devel version. After the catch-up on the recent format changes of the git-output, I need to wait now until the new git-binaries are hitting kernel.org.

Show 5 quoted lines
>  - any reason to limit the "summary" page to just the last 14 changes? The 
>    "log" thing you can ask to go back further, it would be nice to have
>    something like a "last 100" thing for summaries too, especially since 
>    the summary is so nice and dense, so you can actually get a nice view 
>    of what has happened without scrolling _too_ much.
Yes, I recognized that too. Jeff asked for branches and I did the summary
page just for the branches. :) With that I realized that the dense log of the
summary is sometimes nicer than the full log. How about this:
  http://ehlo.org/~kay/gitweb.cgi?p=git/git.git;a=shortlog

It is reachable on the summary page by clicking in the title of the shortlog.

Show 7 quoted lines
>  - I was in the "commitdiff" thing, and initially thought that there was 
>    no way to get back to the "summary" view.
> 
>    It turns out I was wrong (the summary is reachable by just clicking at
>    the project name itself in the top header), but it's a bit strange that
>    the "commitdiff" thing has an explicit link back to itself (hey, 
>    consistency is good, so I'm not complaining)

Well, yes I was trying to get a navigation which is not changing with every new page... It's not an active link now. Maybe that's better.

Show 6 quoted lines
>    but the link back to the
>    summary page is implicit.
> 
>    So how about adding an explicit "summary" link to the list of other 
>    explicit links (log, commit, commitdiff and tree) at the top of the 
>    page?

Done! That was easy. It will show up on kernel.org when the actual git-binaries are installed.

Show 10 quoted lines
> I actually like browsing other peoples projects with the gitweb
> interfaces, it's both responsive and verbose enough to really say
> something good. In contrast, cvsweb is always just a mess of "these are
> the files, go at it", which is totally pointless and doesn't tell anything
> about what is actually happening in the project.
> 
> So dammit, I'm very biased indeed, but I'm just looking at gitweb, and
> comparing it to both the CVS and SVN web things, and they just reinforce
> my conviction that CVS is absolute crap, and I find myself surprised by
> how crap SVN also appears.

Good to hear that. It's a long road to make software from "working" to "nice to use". That was probably never the goal of some SCM-web-interfaces. :)

>   Combining some of the features of the two (that über-cool revision 
>   history graph from gitk rules, for example) might be cool. I get the 
>   urge to do octopus-merges in the kernel just because of how good they
>   look in gitk ;) ]

I would like to show something like the graph too, but I don't really know how to do this in html. Seems slippery if not impossible. If anybody has a nice idea how to represent that, I will give it a try.

Kay
Daniel Serpell· May 28, 2005, 01:03 UTC · re: Kay Sievers · lore

Re: More gitweb queries..

Hi!
On 5/27/05, Kay Sievers <kay.sievers@vrfy.org> wrote:
Show 9 quoted lines
> On Fri, May 27, 2005 at 12:24:20PM -0700, Linus Torvalds wrote:
> >   Combining some of the features of the two (that über-cool revision
> >   history graph from gitk rules, for example) might be cool. I get the
> >   urge to do octopus-merges in the kernel just because of how good they
> >   look in gitk ;) ]
> 
> I would like to show something like the graph too, but I don't really know
> how to do this in html. Seems slippery if not impossible.
> If anybody has a nice idea how to represent that, I will give it a try.

Well, you could draw them in javascript, using http://www.walterzorn.com/jsgraphics/jsgraphics_e.htm :-)

Alternatively, you could use a fixed set of little images, a bar "|", a dot "o" and branches like "Y", "7" and "\". Obviously, octopus-merges are very difficult to draw using only those.

BTW, I tried searching on gitweb, and I think that found a problem, see: http://ehlo.org/~kay/gitweb.cgi?p=git/git.git;a=search;s=check At the bottom of the page, highlighting of the search term stops and the commits are all the same color.

        Daniel.
David Lang· May 28, 2005, 02:51 UTC · re: Daniel Serpell · lore

Re: More gitweb queries..

Show 19 quoted lines
> Hi!
>
> On 5/27/05, Kay Sievers <kay.sievers@vrfy.org> wrote:
>> On Fri, May 27, 2005 at 12:24:20PM -0700, Linus Torvalds wrote:
>>>   Combining some of the features of the two (that über-cool revision
>>>   history graph from gitk rules, for example) might be cool. I get the
>>>   urge to do octopus-merges in the kernel just because of how good they
>>>   look in gitk ;) ]
>>
>> I would like to show something like the graph too, but I don't really know
>> how to do this in html. Seems slippery if not impossible.
>> If anybody has a nice idea how to represent that, I will give it a try.
>
> Well, you could draw them in javascript, using
> http://www.walterzorn.com/jsgraphics/jsgraphics_e.htm :-)
>
> Alternatively, you could use a fixed set of little images, a bar "|", a
> dot "o" and branches like "Y", "7" and "\". Obviously, octopus-merges
> are very difficult to draw using only those.

you could look into SVG (scaleable vector graphics or some such thing) that are supposed to be in the newest browsers (or soon to be added, I'm not sure). this should let you do all the drawing nessasary reasonably easily (if you are willing to limit users to that, which is probably not that big of a problem for git)

David Lang
-- 
There are two ways of constructing a software design. One way is to make it so simple that there are obviously no deficiencies. And the other way is to make it so complicated that there are no obvious deficiencies.
  -- C.A.R. Hoare
Kay Sievers· May 28, 2005, 10:56 UTC · re: David Lang · lore

Re: More gitweb queries..

On Fri, May 27, 2005 at 07:51:39PM -0700, David Lang wrote:
Show 25 quoted lines
> >Hi!
> >
> >On 5/27/05, Kay Sievers <kay.sievers@vrfy.org> wrote:
> >>On Fri, May 27, 2005 at 12:24:20PM -0700, Linus Torvalds wrote:
> >>>  Combining some of the features of the two (that über-cool revision
> >>>  history graph from gitk rules, for example) might be cool. I get the
> >>>  urge to do octopus-merges in the kernel just because of how good they
> >>>  look in gitk ;) ]
> >>
> >>I would like to show something like the graph too, but I don't really know
> >>how to do this in html. Seems slippery if not impossible.
> >>If anybody has a nice idea how to represent that, I will give it a try.
> >
> >Well, you could draw them in javascript, using
> >http://www.walterzorn.com/jsgraphics/jsgraphics_e.htm :-)
> >
> >Alternatively, you could use a fixed set of little images, a bar "|", a
> >dot "o" and branches like "Y", "7" and "\". Obviously, octopus-merges
> >are very difficult to draw using only those.
> 
> you could look into SVG (scaleable vector graphics or some such thing) 
> that are supposed to be in the newest browsers (or soon to be added, I'm 
> not sure). this should let you do all the drawing nessasary reasonably 
> easily (if you are willing to limit users to that, which is probably not 
> that big of a problem for git)

Well, technology that is "soon to be added" since years is probably not my favorite thing to base my work on. SVG could be nice, sure, but nearly noone is able to see it today and I expect, I will need to wait wait for another few years. :)

Kay
Kay Sievers· May 28, 2005, 08:42 UTC · re: Daniel Serpell · lore

Re: More gitweb queries..

On Fri, May 27, 2005 at 09:03:24PM -0400, Daniel Serpell wrote:
Show 15 quoted lines
> Hi!
> 
> On 5/27/05, Kay Sievers <kay.sievers@vrfy.org> wrote:
> > On Fri, May 27, 2005 at 12:24:20PM -0700, Linus Torvalds wrote:
> > >   Combining some of the features of the two (that über-cool revision
> > >   history graph from gitk rules, for example) might be cool. I get the
> > >   urge to do octopus-merges in the kernel just because of how good they
> > >   look in gitk ;) ]
> > 
> > I would like to show something like the graph too, but I don't really know
> > how to do this in html. Seems slippery if not impossible.
> > If anybody has a nice idea how to represent that, I will give it a try.
> 
> Well, you could draw them in javascript, using
> http://www.walterzorn.com/jsgraphics/jsgraphics_e.htm :-)

You know how that stuff works? :) It is a very nice idea for small stuff, but it uses a <div> for every pixel/line you draw and places this in the background and I expect it to kill your browser if you try to draw things like gitk does.

> Alternatively, you could use a fixed set of little images, a bar "|", a
> dot "o" and branches like "Y", "7" and "\". Obviously, octopus-merges
> are very difficult to draw using only those.

Did you look at gitk? With a all the crossing and long lines, you definitely need to draw the lines with colors. Otherwise you will see _nothing_, but random characters. :)

> BTW, I tried searching on gitweb, and I think that found a problem, see:
> http://ehlo.org/~kay/gitweb.cgi?p=git/git.git;a=search;s=check
> At the bottom of the page, highlighting of the search term stops and the
> commits are all the same color.

Well, you see a list of files which contain the text, not the text itself. I can print the filename in red. :)

Thanks, Kay

Benjamin Herrenschmidt· May 28, 2005, 22:43 UTC · re: Kay Sievers · lore

Re: More gitweb queries..

On Sat, 2005-05-28 at 10:42 +0200, Kay Sievers wrote:
Show 20 quoted lines
> You know how that stuff works? :) It is a very nice idea for
> small stuff, but it uses a <div> for every pixel/line you draw and
> places this in the background and I expect it to kill your browser if
> you try to draw things like gitk does.
> 
> > Alternatively, you could use a fixed set of little images, a bar "|", a
> > dot "o" and branches like "Y", "7" and "\". Obviously, octopus-merges
> > are very difficult to draw using only those.
> 
> Did you look at gitk? With a all the crossing and long lines, you definitely
> need to draw the lines with colors. Otherwise you will see _nothing_, but
> random characters. :)
> 
> > BTW, I tried searching on gitweb, and I think that found a problem, see:
> > http://ehlo.org/~kay/gitweb.cgi?p=git/git.git;a=search;s=check
> > At the bottom of the page, highlighting of the search term stops and the
> > commits are all the same color.
> 
> Well, you see a list of files which contain the text, not the text
> itself. I can print the filename in red. :)
Best may be to have the server generate a picture ... ?
Ben.
Paul Mackerras· May 30, 2005, 23:03 UTC · re: Linus Torvalds · lore

Re: More gitweb queries..

Linus Torvalds writes:
Show 5 quoted lines
>   On that small note, I also find "gitk" very cool indeed, too bad about 
>   the fact that tk/tcl seems to always end up looking so _ugly_. Is there 
>   any way to get anti-aliased fonts and a less 'Motify' blocky look from 
>   tcl/tk? Every time I see that, I feel like I'm back in the last century  
>   or something.

Tcl/Tk 8.5 will have anti-aliased fonts (8.5a2 is available on sourceforge). There is apparently some support for widget styling in 8.4 which I will have to look into. The Motif look doesn't bother me, but it does seem to bother you and Ben H. :)

>   Combining some of the features of the two (that über-cool revision 
>   history graph from gitk rules, for example) might be cool. I get the 
>   urge to do octopus-merges in the kernel just because of how good they
>   look in gitk ;) ]

Yes, it's nice that a relatively simple algorithm without much lookahead produces reasonable results.

Paul.
Jeff Epler· May 31, 2005, 02:27 UTC · re: Paul Mackerras · lore

Re: More gitweb queries..

On Tue, May 31, 2005 at 09:03:06AM +1000, Paul Mackerras wrote:
> Tcl/Tk 8.5 will have anti-aliased fonts (8.5a2 is available on
> sourceforge).  There is apparently some support for widget styling in
> 8.4 which I will have to look into.  The Motif look doesn't bother me,
> but it does seem to bother you and Ben H. :)
I have a somewhat stale copy of Tk 8.5 CVS and Tile CVS here.  I tried
gitk on it.  I added the lines
    package require tile; namespace import -force ::ttk::*
    tile::setTheme alt
near the top, which is a simple way to use the new "tile" theme engine
with an included, redmond-style theme.
Here's a screenshot:
    http://emergent.unpy.net/index.cgi-files/sandbox/gitk-tk85+tile.png

I have no idea why the menu is in greek. The rest of the buttons and labels were too, before I switched to Tile for rendering them.

I get an odd traceback that I don't get using the same gitk with
tcl-8.4.5:
    number too large to represent as a Posix time
        while executing
    "return -options $opts $retval"
        (procedure "ConvertUTCToLocal" line 20)
        invoked from within
    "ConvertUTCToLocal $date[set date {}] $timezone"
        (procedure "::tcl::clock::format" line 10)
        invoked from within
    "clock format $audate -format "%Y-%m-%d %H:%M:%S""
        (procedure "readcommit" line 37)
        invoked from within
    "readcommit $p"
        (procedure "drawgraph" line 52)
        invoked from within
    "drawgraph"
        ("after" script)
which stops the graph before it's complete.  Laying out the graph seems
much slower than with tk8.4.
Jeff

← back to recent threads