shithub: git9

Download patch

ref: 72ba278a188950bf0d09f4459e396269d6ec8505
parent: c65ded47aa82d89e1998ed0e8bbc8b1009d25642
author: Ori Bernstein <ori@eigenstate.org>
date: Sun Nov 27 23:49:57 EST 2022

git: sync with 9front

--- a/add
+++ b/add
@@ -16,11 +16,11 @@
 if(~ $#* 0)
 	exec aux/usage
 
-paths=`$nl{cleanname -d $gitrel $*}
+paths=`$nl{cleanname -d $gitrel $* | drop $gitroot}
 if(~ $add tracked)
-	files=`$nl{walk -f $paths}
+	files=`$nl{walk -f ./$paths}
 if not
-	files=`$nl{cd .git/index9/tracked/ && walk -f $paths}
+	files=`$nl{cd .git/index9/tracked/ && walk -f ./$paths}
 
 for(f in $files){
 	if(! ~ `$nl{cleanname $f} .git/*){
--- a/branch
+++ b/branch
@@ -32,7 +32,7 @@
 orig=`{git/query HEAD}
 if (~ $#baseref 1)
 	base=`{git/query $baseref} || exit 'bad base'
-if not if(test -e .git/$new)
+if not if(~ $#newbr 0)
 	base=`{git/query $new}
 if not
 	base=`{git/query HEAD}
@@ -41,7 +41,6 @@
 	if(! ~ $#baseref 0)
 		die update would clobber $branch with $baseref
 	baseref=`$nl{echo -n $new | sed s@refs/heads/@refs/remotes/origin/@}
-	echo $baseref
 	if(! test -e .git/$new)
 		if(! base=`{git/query $baseref})
 			die could not find branch $branch
@@ -49,9 +48,12 @@
 modified=`$nl{git/query -c HEAD $base | grep '^[^-]' | subst '^..'}
 deleted=`$nl{git/query -c HEAD $base | grep '^-' | subst '^..'}
 
-if(! ~ $#modified 0 || ! ~ $#deleted 0 && ~ $#merge 0){
-	git/walk -fRMA $modified $deleted || 
-		die 'uncommited changes would be clobbered'
+# if we're not merging, don't clobber existing changes.
+if(~ $#merge 0){
+	if(! ~ $#modified 0 || ! ~ $#deleted 0){
+		git/walk -fRMA $modified $deleted || 
+			die 'uncommitted changes would be clobbered'
+	}
 }
 if(~ $delete 1){
 	rm -f .git/$new
@@ -69,8 +71,6 @@
 dirtypaths=()
 if(! ~ $#modified 0 || ! ~ $#deleted 0)
 	dirtypaths=`$nl{git/walk -cfRMA $modified $deleted}
-if(! ~ $#modified 0 || ! ~ $#deleted 0)
-	dirtypaths=`$nl{git/walk -cfRMA $modified $deleted}
 if(~ $#dirtypaths 0)
 	cleanpaths=($modified $deleted)
 if not {
@@ -100,10 +100,9 @@
 		rm -rf .git/index9/tracked/$m
 	}
 	if(~ $b file){
-		if(cp -x -- $basedir/tree/$m $m)
-			walk -eq $m > .git/index9/tracked/$m
-		if not
-			echo -n > .git/index9/tracked/$m
+		cp -x -- $basedir/tree/$m $m
+		walk -eq $m > .git/index9/tracked/$m
+		touch $m
 	}
 }
 
@@ -119,4 +118,5 @@
 }
 
 echo ref: $new > .git/HEAD
+echo $new: `{git/query $new}
 exit ''
--- a/clone
+++ b/clone
@@ -7,7 +7,7 @@
 if(~ $debug 1)
 	debug=(-d)
 
-remote=`{echo $1 | subst -g '/*$'}
+remote=`{echo $1 | sed  's@/*$@@'}
 local=$2
 
 if(~ $#remote 0)
@@ -33,7 +33,7 @@
 		echo '[remote "origin"]'
 		echo '	url='$remote
 	}
-	{git/fetch  $debug $branchflag $remote >[2=3] | awk '
+	{git/get  $debug $branchflag $remote >[2=3] | awk '
 		BEGIN{
 			headref=""
 			if(ENVIRON["branch"] != "")
@@ -79,7 +79,7 @@
 
 	tree=.git/fs/HEAD/tree
 	lbranch=`{git/branch}
-	rbranch=`{echo $lbranch | subst '^heads' 'remotes/origin'}
+	rbranch=`{echo $lbranch | subst 'heads' 'remotes/origin'}
 	echo checking out repository...
 	if(test -f .git/refs/$rbranch){
 		cp .git/refs/$rbranch .git/refs/$lbranch
@@ -86,12 +86,10 @@
 		git/fs
 		@ {builtin cd $tree && tar cif /fd/1 .} | @ {tar xf /fd/0} \
 			|| die 'checkout failed:' $status
-		for(f in `$nl{walk -f $tree | subst '^'$tree'/*'}){
-			if(! ~ $#f 0){
-				idx=.git/index9/tracked/$f
-				mkdir -p `$nl{basename -d $idx}
-				walk -eq $f > $idx
-			}
+		for(f in `$nl{walk -f $tree | drop $tree}){
+			idx=.git/index9/tracked/$f
+			mkdir -p `$nl{basename -d $idx}
+			walk -eq ./$f > $idx
 		}
 	}
 	if not{
--- a/commit
+++ b/commit
@@ -2,21 +2,6 @@
 rfork ne
 . /sys/lib/git/common.rc
 
-fn whoami{
-	name=`{git/conf user.name}
-	email=`{git/conf user.email}
-	if(test -f /adm/keys.who){
-		if(~ $name '')
-			name=`{awk -F'|' '$1=="'$user'" {x=$3} END{print x}' </adm/keys.who}
-		if(~ $email '')
-			email=`{awk -F'|' '$1=="'$user'" {x=$5} END{print x}' </adm/keys.who}
-	}
-	if(~ $name '')
-		name=glenda
-	if(~ $email '')
-		email=glenda@9front.local
-}
-
 fn findbranch{
 	branch=`{git/branch}
 	if(test -e $gitfs/branch/$branch/tree){
@@ -58,7 +43,7 @@
 			echo '#'
 			for(p in $parents)
 				echo '# parent:' $p
-			git/walk -fAMR $files | subst -g '^' '# '
+			git/walk -fAMR $files | subst '^' '# '
 			echo '#'
 			echo '# Commit message:'
 		}
@@ -92,7 +77,7 @@
 	msg=`''{cat $msgfile}
 	if(! ~ $#parents 0)
 		pflags='-p'^$parents
-	hash=`{git/save -n $"name -e $"email  -m $"msg $pflags $files || die $status}
+	hash=`{git/save -n $"name -e $"email -m $"msg $pflags $files || die $status}
 	rm -f .git/index9/merge-parents
 }
 
@@ -116,7 +101,8 @@
 }
 
 fn sigexit{
-	rm -f $msgfile $msgfile.tmp
+	if(! ~ $#msgfile 0)
+		rm -f $msgfile $msgfile.tmp
 }
 
 gitup
--- a/common.rc
+++ b/common.rc
@@ -11,35 +11,60 @@
 	exit 'usage'
 }
 
-# subst [-g] this [that]
-fn subst{
-	awk 'BEGIN{
-		global = 0
-		for(i = 1; ARGV[i] ~ /^-/; i++){
-			if(ARGV[i] == "-g")
-				global = 1
-			ARGC--
-		}
-		this = ARGV[i++]; ARGC--
-		that = ARGV[i++]; ARGC--
-	}
+fn subst {
+	awk '
+	BEGIN{ARGC=0}
+	{sub(ARGV[1], ARGV[2]); print}
+	' $*
+}
+
+fn drop {
+	awk '
+	BEGIN{ARGC=0}
 	{
-		if(global) gsub(this, that)
-		else sub(this, that)
+		if(index($0, ARGV[1]) == 1)
+			$0=substr($0, length(ARGV[1])+1)
 		print
-	}' $*
+	}
+	' $*
 }
 
-fn present {
+fn mergeperm {
 	if(~ $1 /dev/null && cmp $2 $3>/dev/null)
 		status=gone
 	if not if (~ $3 /dev/null && cmp $1 $2>/dev/null)
 		status=gone
-	if not
+	if not {
+		mergedperms='-x'
+		if(test -x $2){
+			if(test -x $1 -a -x $3)
+				mergedperms='+x'
+		}
+		if not{
+			if(test -x $1 -o -x $3)
+				mergedperms='+x'
+		}
 		status=()
+	}
 }
 
-# merge1 out theirs base ours
+fn whoami{
+	name=`$nl{git/conf user.name}
+	email=`$nl{git/conf user.email}
+	if(test -f /adm/keys.who){
+		if(~ $name '')
+			name=`$nl{awk -F'|' '$1=="'$user'" {x=$3} END{print x}' </adm/keys.who}
+		if(~ $email '')
+			email=`$nl{awk -F'|' '$1=="'$user'" {x=$5} END{print x}' </adm/keys.who}
+	}
+	if(~ $name '')
+		name=$user
+	if(~ $email '')
+		email=$user@$sysname
+	status=''
+}
+
+# merge1 out ours base theirs
 fn merge1 {@{
 	rfork e
 	n=$pid
@@ -62,9 +87,10 @@
 	if(! ape/diff3 -3 -m $ours $base $theirs > $tmp)
 		echo merge needed: $out >[1=2]
 
-	if(present $ours $base $theirs){
+	if(mergeperm $ours $base $theirs){
 		mv $tmp $out
 		git/add $out
+		chmod $mergedperms $out
 	}
 	if not {
 		rm -f $tmp $out
@@ -77,13 +103,13 @@
 	if(~ $#gitroot 0)
 		die 'not a git repository'
 	gitfs=$gitroot/.git/fs
-	gitrel=`{pwd | subst '^'$"gitroot'/?'}
+	gitrel=`{pwd | drop $gitroot | sed 's@^/@@'}
 	if(~ $#gitrel 0)
 		gitrel='.'
-	cd $gitroot
+	if(! cd $gitroot)
+		die cd $gitroot: no repo there
 	startfs=()
-	if(! test -d $gitfs)
-		mkdir -p $gitfs
+	mkdir -p $gitfs
 	if(! test -e $gitfs/ctl)
 		startfs=true
 	if(! grep -s '^repo '$gitroot'$' $gitfs/ctl >[2]/dev/null)
--- a/compat
+++ b/compat
@@ -4,12 +4,14 @@
 
 opts=()
 args=()
+nl='
+'
 
 fn cmd_init{
-	while(~ $#* 0){
+	while(! ~ $#* 0){
 		switch($1){
 		case --bare
-			opts=(-b)
+			# ignore
 		case -- 
 			# go likes to use these
 		case -*
@@ -19,7 +21,6 @@
 		}
 		shift
 	}
-	ls >[1=2]
 	git/init $opts $args
 }
 
@@ -94,7 +95,7 @@
 			echo `{dcmd git9/branch | sed s@^heads/@@g}
 			shift
 		case *
-			dprint option $opt
+			die unknown option $opt
 		}
 		shift
 	}
@@ -112,6 +113,15 @@
 		echo `{cat $gitroot/.git/refs/$b} refs/$b 
 }
 
+fn cmd_rev-parse{
+	switch($1){
+	case --git-dir
+		echo `{git/conf -r}^/.git
+	case *
+		die 'unknown rev-parse '$*
+	}
+}
+
 fn cmd_remote{
 	if({! ~ $#* 3 && ! ~ $#* 4} || ! ~ $1 add)
 		die unimplemented remote cmd $*
@@ -125,10 +135,60 @@
 	}
 }
 
+fn cmd_log{
+	count=()
+	format=''
+	while(~ $1 -*){
+		switch($1){
+		case --format
+			format=$2
+			shift
+		case '--format='*
+			format=`{echo $1 | sed 's/--format=//g'}
+		case -n
+			count=-n$2
+			shift
+		case -n*
+			count=$1
+		case *
+			dprint option $opt
+		}
+		shift
+	}
+	@{cd $gitroot && git/fs}
+	switch($format){
+	case ''
+		git/log $count
+	case '%H:%ct'
+		for(c in `{git/log -s $count| awk '{print $1}'})
+			echo $c:`{mtime $gitroot/.git/fs/object/$c/msg}
+	case '%h %cd'
+		for(c in `{git/log -s $count| awk '{print $1}'})
+			echo $c `{date `{mtime $gitroot/.git/fs/object/$c/msg}}
+	}
+
+}
+
+fn cmd_show{
+	cmd_log -n1 $*
+}
+
+fn cmd_ls-remote{
+	if(~ $1 -q)
+		shift
+	remote=`$nl{git/conf 'remote "'$1'".url'}
+	if(~ $#remote 0)
+		remote=$1
+	git/get -l $remote | awk '/^remote/{print $3"\t"$2}'
+}
+
 fn cmd_version{
 	echo git version 2.2.0
 }
 
+fn cmd_status{
+	echo
+}
 
 fn usage{
 	echo 'git <command> <args>' >[1=2]
@@ -149,10 +209,20 @@
 	exec rc
 }
 
+if(~ $#gitcompatdebug 1)
+	echo running  $* >>/tmp/gitlog
+
+if(~ $1 -c)
+	shift 2
+if(~ $1 -c*)
+	shift 1
 if(! test -f '/env/fn#cmd_'$1)
 	die git $1: commmand not implemented
 if(! ~ $1 init && ! ~ $1 clone)
 	gitroot=`{git/conf -r} || die repo
 
-cmd_$1 $*(2-)
+if(~ $#gitcompatdebug 1)
+	cmd_$1 $*(2-) | tee >>/tmp/gitlog
+if not
+	cmd_$1 $*(2-)
 exit ''
--- a/delta.c
+++ b/delta.c
@@ -7,7 +7,6 @@
 	Minchunk	= 128,
 	Maxchunk	= 8192,
 	Splitmask	= (1<<8)-1,
-	
 };
 
 static u32int geartab[] = {
@@ -48,9 +47,7 @@
 static u64int
 hash(void *p, int n)
 {
-	uchar buf[SHA1dlen];
-	sha1((uchar*)p, n, buf, nil);
-	return GETBE64(buf);
+	return murmurhash2(p, n);
 }
 
 static void
@@ -172,14 +169,18 @@
 static int
 stretch(Dtab *dt, Dblock *b, uchar *s, uchar *e, int n)
 {
-	uchar *p, *q, *eb;
+	uchar *p0, *p, *q, *eb;
 
 	if(b == nil)
 		return n;
 	p = s + n;
 	q = dt->base + b->off + n;
-	eb = dt->base + dt->nbase;
-	while(n < (1<<24)-1){
+	p0 = p;
+	if(dt->nbase < (1<<24)-1)
+		eb = dt->base + dt->nbase;
+	else
+		eb = dt->base + (1<<24)-1;
+	while(1){
 		if(p == e || q == eb)
 			break;
 		if(*p != *q)
@@ -186,9 +187,8 @@
 			break;
 		p++;
 		q++;
-		n++;
 	}
-	return n;
+	return n + (p - p0);
 }
 
 Delta*
--- a/diff
+++ b/diff
@@ -26,12 +26,22 @@
 		git/query -c $commit HEAD | subst '^..'
 }
 
+showed=()
+mntgen /mnt/scratch
+bind $branch/tree/ /mnt/scratch/a
+bind . /mnt/scratch/b
 for(f in `$nl{lsdirty | sort | uniq}){
-	orig=$branch/tree/$f
-	if(! test -f $orig)
-		orig=/dev/null
-	if(! test -f $f)
-		f=/dev/null
-	diff -u $orig $f
+	if(~ $#showed 0){
+		echo diff `{git/query $commit} uncommitted
+		showed=1
+	}
+	cd /mnt/scratch
+	a=a/$f
+	b=b/$f
+	if(! test -f a/$f)
+		a=/dev/null
+	if(! test -f b/$f)
+		b=/dev/null
+	diff -u $a $b
 }
 exit ''
--- a/export
+++ b/export
@@ -41,9 +41,8 @@
 		if(test -d $cp/tree)
 			bind $cp/tree b
 		
-		echo From $c
 		echo From: `{cat $cp/author}
-		echo Date: `{date -um `{mtime $cp/author | awk '{print $1}'}}
+		echo Date: `{date -uf'WW, DD MMM YYYY hh:mm:ss Z' `{walk -em $cp/author}}
 		<$cp/msg awk '
 		NR == 1 {
 			n = ENVIRON["n"]
@@ -72,7 +71,7 @@
 			b=b/$f
 			if(! test -e $b)
 				b=/dev/null
-			ape/diff -urN $a $b
+			diff -ur $a $b
 		}
 	} >$patchfile
 	if(~ $#patchdir 0){
--- a/fs.c
+++ b/fs.c
@@ -12,12 +12,13 @@
 	Qhead,
 	Qbranch,
 	Qcommit,
-	Qcommitmsg,
-	Qcommitparent,
-	Qcommittree,
-	Qcommitdata,
-	Qcommithash,
-	Qcommitauthor,
+		Qmsg,
+		Qparent,
+		Qtree,
+		Qcdata,
+		Qhash,
+		Qauthor,
+		Qcommitter,
 	Qobject,
 	Qctl,
 	Qmax,
@@ -69,13 +70,14 @@
 	"ctl",
 };
 
-#define Eperm	"permission denied";
-#define Eexist	"does not exist";
-#define E2long	"path too long";
-#define Enodir	"not a directory";
-#define Erepo	"unable to read repo";
-#define Egreg	"wat";
-#define Ebadobj	"invalid object";
+#define Eperm	"permission denied"
+#define Eexist	"does not exist"
+#define E2long	"path too long"
+#define Enodir	"not a directory"
+#define Erepo	"unable to read repo"
+#define Eobject "invalid object"
+#define Egreg	"wat"
+#define Ebadobj	"invalid object"
 
 char	gitdir[512];
 char	*username;
@@ -284,23 +286,23 @@
 		d->mode = 0755 | DMDIR;
 		d->name = estrdup9p("tree");
 		d->qid.type = QTDIR;
-		d->qid.path = qpath(c, i, o->id, Qcommittree);
+		d->qid.path = qpath(c, i, o->id, Qtree);
 		break;
 	case 1:
 		d->name = estrdup9p("parent");
-		d->qid.path = qpath(c, i, o->id, Qcommitparent);
+		d->qid.path = qpath(c, i, o->id, Qparent);
 		break;
 	case 2:
 		d->name = estrdup9p("msg");
-		d->qid.path = qpath(c, i, o->id, Qcommitmsg);
+		d->qid.path = qpath(c, i, o->id, Qmsg);
 		break;
 	case 3:
 		d->name = estrdup9p("hash");
-		d->qid.path = qpath(c, i, o->id, Qcommithash);
+		d->qid.path = qpath(c, i, o->id, Qhash);
 		break;
 	case 4:
 		d->name = estrdup9p("author");
-		d->qid.path = qpath(c, i, o->id, Qcommitauthor);
+		d->qid.path = qpath(c, i, o->id, Qauthor);
 		break;
 	default:
 		return -1;
@@ -376,19 +378,20 @@
 static void
 readcommitparent(Req *r, Object *o)
 {
-	char *buf, *p;
+	char *buf, *p, *e;
 	int i, n;
 
-	n = o->commit->nparent * (40 + 2);
+	/* 40 bytes per hash, 1 per nl, 1 for terminator */
+	n = o->commit->nparent * (40 + 1) + 1;
 	buf = emalloc(n);
 	p = buf;
+	e = buf + n;
 	for (i = 0; i < o->commit->nparent; i++)
-		p += sprint(p, "%H\n", o->commit->parent[i]);
-	readbuf(r, buf, n);
+		p = seprint(p, e, "%H\n", o->commit->parent[i]);
+	readbuf(r, buf, p - buf);
 	free(buf);
 }
 
-
 static void
 gitattach(Req *r)
 {
@@ -491,18 +494,20 @@
 		q->type = 0;
 		c->mtime = o->commit->mtime;
 		c->mode = 0644;
-		assert(qdir == Qcommit || qdir == Qobject || qdir == Qcommittree || qdir == Qhead);
+		assert(qdir == Qcommit || qdir == Qobject || qdir == Qtree || qdir == Qhead || qdir == Qcommitter);
 		if(strcmp(name, "msg") == 0)
-			q->path = qpath(p, 0, o->id, Qcommitmsg);
+			q->path = qpath(p, 0, o->id, Qmsg);
 		else if(strcmp(name, "parent") == 0)
-			q->path = qpath(p, 1, o->id, Qcommitparent);
+			q->path = qpath(p, 1, o->id, Qparent);
 		else if(strcmp(name, "hash") == 0)
-			q->path = qpath(p, 2, o->id, Qcommithash);
+			q->path = qpath(p, 2, o->id, Qhash);
 		else if(strcmp(name, "author") == 0)
-			q->path = qpath(p, 3, o->id, Qcommitauthor);
+			q->path = qpath(p, 3, o->id, Qauthor);
+		else if(strcmp(name, "committer") == 0)
+			q->path = qpath(p, 3, o->id, Qcommitter);
 		else if(strcmp(name, "tree") == 0){
 			q->type = QTDIR;
-			q->path = qpath(p, 4, o->id, Qcommittree);
+			q->path = qpath(p, 4, o->id, Qtree);
 			unref(c->obj);
 			c->mode = DMDIR | 0755;
 			c->obj = readobject(o->commit->tree);
@@ -620,9 +625,9 @@
 			e = objwalk1(q, o->obj, o, c, name, Qobject, aux);
 		}else{
 			if(hparse(&h, name) == -1)
-				return "invalid object name";
+				return Eobject;
 			if((c->obj = readobject(h)) == nil)
-				return "could not read object";
+				return Eobject;
 			if(c->obj->type == GBlob || c->obj->type == GTag){
 				c->mode = 0644;
 				q->type = 0;
@@ -640,14 +645,15 @@
 	case Qcommit:
 		e = objwalk1(q, o->obj, o, c, name, Qcommit, aux);
 		break;
-	case Qcommittree:
-		e = objwalk1(q, o->obj, o, c, name, Qcommittree, aux);
+	case Qtree:
+		e = objwalk1(q, o->obj, o, c, name, Qtree, aux);
 		break;
-	case Qcommitparent:
-	case Qcommitmsg:
-	case Qcommitdata:
-	case Qcommithash:
-	case Qcommitauthor:
+	case Qparent:
+	case Qmsg:
+	case Qcdata:
+	case Qhash:
+	case Qauthor:
+	case Qcommitter:
 	case Qctl:
 		return Enodir;
 	default:
@@ -760,20 +766,24 @@
 		else
 			dirread9p(r, objgen, aux);
 		break;
-	case Qcommitmsg:
+	case Qmsg:
 		readbuf(r, o->commit->msg, o->commit->nmsg);
 		break;
-	case Qcommitparent:
+	case Qparent:
 		readcommitparent(r, o);
 		break;
-	case Qcommithash:
+	case Qhash:
 		snprint(buf, sizeof(buf), "%H\n", o->hash);
 		readstr(r, buf);
 		break;
-	case Qcommitauthor:
+	case Qauthor:
 		snprint(buf, sizeof(buf), "%s\n", o->commit->author);
 		readstr(r, buf);
 		break;
+	case Qcommitter:
+		snprint(buf, sizeof(buf), "%s\n", o->commit->committer);
+		readstr(r, buf);
+		break;
 	case Qctl:
 		e = readctl(r);
 		break;
@@ -785,8 +795,8 @@
 			objread(r, aux);
 		break;
 	case Qcommit:
-	case Qcommittree:
-	case Qcommitdata:
+	case Qtree:
+	case Qcdata:
 		objread(r, aux);
 		break;
 	default:
@@ -796,6 +806,34 @@
 }
 
 static void
+gitopen(Req *r)
+{
+	Gitaux *aux;
+	Crumb *c;
+
+	aux = r->fid->aux;
+	c = crumb(aux, 0);
+	switch(r->ifcall.mode&3){
+	default:
+		respond(r, "botched mode");
+		break;
+	case OWRITE:
+		respond(r, Eperm);
+		break;
+	case OREAD:
+	case ORDWR:
+		respond(r, nil);
+		break;
+	case OEXEC:
+		if((c->mode & 0111) == 0)
+			respond(r, Eperm);
+		else
+			respond(r, nil);
+		break;
+	}
+}
+
+static void
 gitstat(Req *r)
 {
 	Gitaux *aux;
@@ -821,6 +859,7 @@
 	.attach=gitattach,
 	.walk1=gitwalk1,
 	.clone=gitclone,
+	.open=gitopen,
 	.read=gitread,
 	.stat=gitstat,
 	.destroyfid=gitdestroyfid,
--- a/git.h
+++ b/git.h
@@ -4,6 +4,7 @@
 #include <flate.h>
 #include <regexp.h>
 
+typedef struct Capset	Capset;
 typedef struct Conn	Conn;
 typedef struct Hash	Hash;
 typedef struct Delta	Delta;
@@ -18,6 +19,8 @@
 typedef struct Objlist	Objlist;
 typedef struct Dtab	Dtab;
 typedef struct Dblock	Dblock;
+typedef struct Objq	Objq;
+typedef struct Qelt	Qelt;
 
 enum {
 	Pathmax		= 512,
@@ -24,6 +27,8 @@
 	Npackcache	= 32,
 	Hashsz		= 20,
 	Pktmax		= 65536,
+	KiB		= 1024,
+	MiB		= 1024*KiB,
 };
 
 enum {
@@ -151,6 +156,18 @@
 	int	sz;
 };
 
+struct Qelt {
+	Object	*o;
+	vlong	ctime;
+	int	color;
+};
+
+struct Objq {
+	Qelt	*heap;
+	int	nheap;
+	int	heapsz;
+};
+
 struct Dtab {
 	Object	*o;
 	uchar	*base;
@@ -225,9 +242,9 @@
 
 extern Reprog	*authorpat;
 extern Objset	objcache;
+extern vlong	cachemax;
 extern Hash	Zhash;
 extern int	chattygit;
-extern int	cachemax;
 extern int	interactive;
 
 #pragma varargck type "H" Hash
@@ -286,6 +303,7 @@
 char	*strip(char *);
 int	findrepo(char *, int);
 int	showprogress(int, int);
+u64int	murmurhash2(void*, usize);
 
 /* packing */
 void	dtinit(Dtab *, Object*);
@@ -295,6 +313,7 @@
 /* proto handling */
 int	readpkt(Conn*, char*, int);
 int	writepkt(Conn*, char*, int);
+int	fmtpkt(Conn*, char*, ...);
 int	flushpkt(Conn*);
 void	initconn(Conn*, int, int);
 int	gitconnect(Conn *, char *, char *);
@@ -301,3 +320,9 @@
 int	readphase(Conn *);
 int	writephase(Conn *);
 void	closeconn(Conn *);
+
+/* queues */
+void	qinit(Objq*);
+void	qclear(Objq*);
+void	qput(Objq*, Object*, int);
+int	qpop(Objq*, Qelt*);
--- a/import
+++ b/import
@@ -7,11 +7,13 @@
 	rm -f $diffpath
 }
 
+
 fn apply @{
 	git/fs
-	email=''
-	name=''
+	amail=''
+	aname=''
 	msg=''
+	whoami
 	parents='-p'^`{git/query HEAD}
 	branch=`{git/branch}
 	if(test -e $gitfs/branch/$branch/tree)
@@ -26,11 +28,11 @@
 	}
 	state=="headers" && /^From:/ {
 		sub(/^From:[ \t]*/, "", $0);
-		name=$0;
-		email=$0;
-		sub(/[ \t]*<.*$/, "", name);
-		sub(/.*</, "", email);
-		sub(/>/, "", email);
+		aname=$0;
+		amail=$0;
+		sub(/[ \t]*<.*$/, "", aname);
+		sub(/^[^<]*</, "", amail);
+		sub(/>[^>]*$/, "", amail);
 	}
 	state=="headers" && /^Date:/{
 		sub(/^Date:[ \t]*/, "", $0)
@@ -43,12 +45,19 @@
 	}
 	state=="headers" && /^$/ {
 		state="body"
-		next
 	}
-	(state=="headers" || state=="body") && (/^diff/ || /^---[ 	]*$/){
+	(state=="headers" || state=="body") && (/^diff / || /^---( |$)/){
 		state="diff"
 	}
+	state=="body" && /^[ 	]*$/ {
+		empty=1
+		next
+	}
 	state=="body" {
+		if(empty)
+			printf "\n" > "/env/msg"
+		empty=0
+		sub(/[ 	]+$/, "")
 		print > "/env/msg"
 	}
 	state=="diff" {
@@ -57,10 +66,10 @@
 	END{
 		if(state != "diff")
 			exit("malformed patch: " state);
-		if(name == "" || email == "" || date == "" || gotmsg == "")
+		if(aname == "" || amail == "" || date == "" || gotmsg == "")
 			exit("missing headers");
-		printf "%s", name > "/env/name"
-		printf "%s", email > "/env/email"
+		printf "%s", aname > "/env/aname"
+		printf "%s", amail > "/env/amail"
 		printf "%s", date > "/env/date"
 	}
 	' || die 'could not import:' $status
@@ -67,9 +76,17 @@
 
 	# force re-reading env
 	rc -c '
-		echo applying $msg | sed 1q
 		date=`{seconds $date}
-		if(! files=`$nl{ape/patch -Ep1 < $diffpath | grep ''^patching file'' | sed ''s/^patching file `(.*)''''/\1/''})
+		files=`$nl{patch -np1 < $diffpath}
+		if(! git/walk -q $files){
+			>[1=2] {
+				echo patch would clobber files:
+				git/walk $files
+				exit clobber
+			}
+		}
+		echo applying $msg | sed 1q
+		if(! files=`$nl{patch -p1 < $diffpath})
 			die ''patch failed''
 		for(f in $files){
 			if(test -e $f)
@@ -79,8 +96,10 @@
 		}
 		git/walk -fRMA $files
 		if(~ $#nocommit 0){
-			hash=`{git/save -n $name -e $email -m $msg -d $date $parents $files}
-			echo $hash > $refpath
+			if(hash=`{git/save -n $aname -e $amail -N $name -E $email -m $msg -d $date $parents $files}){
+				echo $hash > $refpath
+				rm -f .git/index9/removed/$files
+			}
 		}
 		status=''''
 	'
@@ -93,7 +112,7 @@
 
 patches=(/fd/0)
 if(! ~ $#* 0)
-	patches=$*
+	patches=`{cleanname -d $gitrel $*}
 for(p in $patches){
 	# upas serves the decoded header and body separately,
 	# so we cat them together when applying a upas message.
@@ -102,6 +121,6 @@
 	if(test -d $p && test -f $p/header && test -f $p/body)
 		{{cat $p/header; echo; cat $p/body} | apply} || die $status
 	if not
-		apply < $p || die $status
+		apply < $p
 }
 exit ''
--- a/init
+++ b/init
@@ -20,7 +20,7 @@
 }
 
 mkdir -p $dir/.git/refs/^(heads remotes)
-mkdir -p $dir/.git/fs
+mkdir -p $dir/.git/^(fs objects)
 >$dir/.git/config {
 	echo '[core]'
 	echo '	repositoryformatversion = p9.0'
--- a/log.c
+++ b/log.c
@@ -14,11 +14,10 @@
 char	*queryexpr;
 char	*commitid;
 int	shortlog;
+int	msgcount = -1;
 
-Object	**heap;
-int	nheap;
-int	heapsz;
 Objset	done;
+Objq	objq;
 Pfilt	*pathfilt;
 
 void
@@ -65,7 +64,7 @@
 }
 
 int
-filtermatch1(Pfilt *pf, Object *t, Object *pt)
+matchesfilter1(Pfilt *pf, Object *t, Object *pt)
 {
 	Object *a, *b;
 	Hash ha, hb;
@@ -89,7 +88,7 @@
 			sysfatal("read %H: %r", ha);
 		if((b = readobject(hb)) == nil)
 			sysfatal("read %H: %r", hb);
-		r = filtermatch1(&pf->sub[i], a, b);
+		r = matchesfilter1(&pf->sub[i], a, b);
 		unref(a);
 		unref(b);
 		if(r)
@@ -99,11 +98,12 @@
 }
 
 int
-filtermatch(Object *o)
+matchesfilter(Object *o)
 {
 	Object *t, *p, *pt;
 	int i, r;
 
+	assert(o->type == GCommit);
 	if(pathfilt == nil)
 		return 1;
 	if((t = readobject(o->commit->tree)) == nil)
@@ -113,7 +113,7 @@
 			sysfatal("read %H: %r", o->commit->parent[i]);
 		if((pt = readobject(p->commit->tree)) == nil)
 			sysfatal("read %H: %r", o->commit->tree);
-		r = filtermatch1(pathfilt, t, pt);
+		r = matchesfilter1(pathfilt, t, pt);
 		unref(p);
 		unref(pt);
 		if(r)
@@ -132,7 +132,7 @@
 	return p;
 }
 
-static void
+static int
 show(Object *o)
 {
 	Tm tm;
@@ -139,9 +139,6 @@
 	char *p, *q, *e;
 
 	assert(o->type == GCommit);
-	if(!filtermatch(o))
-		return;
-
 	if(shortlog){
 		p = o->commit->msg;
 		e = p + o->commit->nmsg;
@@ -153,6 +150,9 @@
 		tmtime(&tm, o->commit->mtime, tzload("local"));
 		Bprint(out, "Hash:\t%H\n", o->hash);
 		Bprint(out, "Author:\t%s\n", o->commit->author);
+		if(o->commit->committer != nil
+		&& strcmp(o->commit->author, o->commit->committer) != 0)
+			Bprint(out, "Committer:\t%s\n", o->commit->committer);
 		Bprint(out, "Date:\t%τ\n", tmfmt(&tm, "WW MMM D hh:mm:ss z YYYY"));
 		Bprint(out, "\n");
 		p = o->commit->msg;
@@ -168,6 +168,7 @@
 		Bprint(out, "\n");
 	}
 	Bflush(out);
+	return 1;
 }
 
 static void
@@ -179,10 +180,14 @@
 
 	if((n = resolverefs(&h, q)) == -1)
 		sysfatal("resolve: %r");
-	for(i = 0; i < n; i++){
+	for(i = 0; i < n && (msgcount == -1 || msgcount > 0); i++){
 		if((o = readobject(h[i])) == nil)
 			sysfatal("read %H: %r", h[i]);
-		show(o);
+		if(matchesfilter(o)){
+			show(o);
+			if(msgcount != -1)
+				msgcount--;
+		}
 		unref(o);
 	}
 	exits(nil);
@@ -189,64 +194,10 @@
 }
 
 static void
-qput(Object *o)
-{
-	Object *p;
-	int i;
-
-	if(oshas(&done, o->hash))
-		return;
-	osadd(&done, o);
-	if(nheap == heapsz){
-		heapsz *= 2;
-		heap = earealloc(heap, heapsz, sizeof(Object*));
-	}
-	heap[nheap++] = o;
-	for(i = nheap - 1; i > 0; i = (i-1)/2){
-		o = heap[i];
-		p = heap[(i-1)/2];
-		if(o->commit->mtime < p->commit->mtime)
-			break;
-		heap[i] = p;
-		heap[(i-1)/2] = o;
-	}
-}
-
-static Object*
-qpop(void)
-{
-	Object *o, *t;
-	int i, l, r, m;
-
-	if(nheap == 0)
-		return nil;
-
-	i = 0;
-	o = heap[0];
-	t = heap[--nheap];
-	heap[0] = t;
-	while(1){
-		m = i;
-		l = 2*i+1;
-		r = 2*i+2;
-		if(l < nheap && heap[m]->commit->mtime < heap[l]->commit->mtime)
-			m = l;
-		if(r < nheap && heap[m]->commit->mtime < heap[r]->commit->mtime)
-			m = r;
-		else
-			break;
-		t = heap[m];
-		heap[m] = heap[i];
-		heap[i] = t;
-		i = m;
-	}
-	return o;
-}
-
-static void
 showcommits(char *c)
 {
 	Object *o, *p;
+	Qelt e;
 	int i;
 	Hash h;
 
@@ -256,18 +207,24 @@
 		sysfatal("resolve %s: %r", c);
 	if((o = readobject(h)) == nil)
 		sysfatal("load %H: %r", h);
-	heapsz = 8;
-	heap = eamalloc(heapsz, sizeof(Object*));
+	qinit(&objq);
 	osinit(&done);
-	qput(o);
-	while((o = qpop()) != nil){
-		show(o);
-		for(i = 0; i < o->commit->nparent; i++){
-			if((p = readobject(o->commit->parent[i])) == nil)
+	qput(&objq, o, 0);
+	while(qpop(&objq, &e) && (msgcount == -1 || msgcount > 0)){
+		if(matchesfilter(e.o)){
+			show(e.o);
+			if(msgcount != -1)
+				msgcount--;
+		}
+		for(i = 0; i < e.o->commit->nparent; i++){
+			if(oshas(&done, e.o->commit->parent[i]))
+				continue;
+			if((p = readobject(e.o->commit->parent[i])) == nil)
 				sysfatal("load %H: %r", o->commit->parent[i]);
-			qput(p);
+			osadd(&done, p);
+			qput(&objq, p, 0);
 		}
-		unref(o);
+		unref(e.o);
 	}
 }
 
@@ -293,6 +250,9 @@
 		break;
 	case 's':
 		shortlog++;
+		break;
+	case 'n':
+		msgcount = atoi(EARGF(usage()));
 		break;
 	default:
 		usage();
--- a/merge
+++ b/merge
@@ -7,13 +7,12 @@
 	basebr=$gitfs/object/$2/tree
 	theirbr=$gitfs/object/$3/tree
 
-	all=`$nl{{git/query -c $1 $2; git/query -c $2 $3} | sed 's/^..//' | \
-		subst -g '^('$ourbr'|'$basebr'|'$theirbr')/*' | sort | uniq}
+	all=`$nl{{git/query -c $1 $2; git/query -c $2 $3} | sed 's/^..//' | sort | uniq}
 	for(f in $all){
 		ours=$ourbr/$f
 		base=$basebr/$f
 		theirs=$theirbr/$f
-		merge1 $f $theirs $base $ours
+		merge1 ./$f $ours $base $theirs
 	}
 }
 
--- a/mkfile
+++ b/mkfile
@@ -3,7 +3,7 @@
 BIN=/$objtype/bin/git
 TARG=\
 	conf\
-	fetch\
+	get\
 	fs\
 	log\
 	query\
@@ -21,6 +21,7 @@
 	compat\
 	diff\
 	export\
+	hist\
 	import\
 	init\
 	merge\
--- a/pack.c
+++ b/pack.c
@@ -20,7 +20,7 @@
 
 struct Meta {
 	Object	*obj;
-	char	*path;
+	vlong	path;
 	vlong	mtime;
 
 	/* The best delta we picked */
@@ -65,8 +65,8 @@
 Objset objcache;
 Object *lruhead;
 Object *lrutail;
-int	ncache;
-int	cachemax = 4096;
+vlong	ncache;
+vlong	cachemax = 512*MiB;
 Packf	*packf;
 int	npackf;
 int	openpacks;
@@ -158,7 +158,7 @@
 	if(!(o->flag & Ccache)){
 		o->flag |= Ccache;
 		ref(o);
-		ncache++;
+		ncache += o->size;
 	}
 	while(ncache > cachemax && lrutail != nil){
 		p = lrutail;
@@ -168,8 +168,8 @@
 		p->flag &= ~Ccache;
 		p->prev = nil;
 		p->next = nil;
+		ncache -= p->size;
 		unref(p);
-		ncache--;
 	}		
 }
 
@@ -194,26 +194,26 @@
 			pf->nidx = packf[i].nidx;
 			packf[i].idx = nil;
 			packf[i].pack = nil;
+			return 0;
 		}
 	}
 	if((ifd = open(buf, OREAD)) == -1)
-		goto error;
-	if((d = dirfstat(ifd)) == nil)
-		goto error;
+		return -1;
+	if((d = dirfstat(ifd)) == nil){
+		close(ifd);
+		return -1;
+	}
 	pf->nidx = d->length;
 	pf->idx = emalloc(pf->nidx);
 	if(readn(ifd, pf->idx, pf->nidx) != pf->nidx){
+		close(ifd);
 		free(pf->idx);
 		free(d);
-		goto error;
+		return -1;
 	}
+	close(ifd);
 	free(d);
 	return 0;
-
-error:
-	if(ifd != -1)
-		close(ifd);
-	return -1;	
 }
 
 static void
@@ -253,28 +253,44 @@
 	vlong t;
 	int i, best;
 
-	if(pf->pack == nil){
-		if((pf->pack = Bopen(pf->path, OREAD)) == nil)
-			return nil;
-		openpacks++;
+	if(pf->pack != nil){
+		pf->refs++;
+		return pf->pack;
 	}
-	if(openpacks == Npackcache){
-		t = pf->opentm;
+	/*
+	 * If we've got more packs open
+	 * than we want cached, try to
+	 * free up the oldest ones.
+	 *
+	 * If we can't find a slot, this
+	 * isn't fatal; we can just use
+	 * another fd.
+	 */
+	while(openpacks >= Npackcache){
+		t = (1ull<<62)-1;
 		best = -1;
 		for(i = 0; i < npackf; i++){
-			if(packf[i].opentm < t && packf[i].refs > 0){
+			if(&packf[i] != pf
+			&& packf[i].pack != nil
+			&& packf[i].opentm < t
+			&& packf[i].refs == 0){
 				t = packf[i].opentm;
 				best = i;
 			}
 		}
-		if(best != -1){
-			Bterm(packf[best].pack);
-			packf[best].pack = nil;
-			openpacks--;
+		if(best == -1){
+			fprint(2, "no available pack slots\n");
+			break;
 		}
+		Bterm(packf[best].pack);
+		packf[best].pack = nil;
+		openpacks--;
 	}
+	openpacks++;
 	pf->opentm = nsec();
 	pf->refs++;
+	if((pf->pack = Bopen(pf->path, OREAD)) == nil)
+		return nil;
 	return pf->pack;
 }
 
@@ -281,10 +297,7 @@
 static void
 closepack(Packf *pf)
 {
-	if(--pf->refs == 0){
-		Bterm(pf->pack);
-		pf->pack = nil;
-	}
+	pf->refs--;
 }
 
 static u32int
@@ -871,6 +884,8 @@
 		}else if(strcmp(buf, "gpgsig") == 0){
 			/* just drop it */
 			if((t = strstr(p, "-----END PGP SIGNATURE-----")) == nil)
+			if((t = strstr(p, "-----END SSH SIGNATURE-----")) == nil)
+			if((t = strstr(p, "-----END SIGNED MESSAGE-----")) == nil)
 				sysfatal("malformed gpg signature");
 			np -= t - p;
 			p = t;
@@ -1023,7 +1038,6 @@
 			return obj;
 		}
 	}
-			
 
 	snprint(hbuf, sizeof(hbuf), "%H", h);
 	snprint(path, sizeof(path), ".git/objects/%c%c/%s", hbuf[0], hbuf[1], hbuf + 2);
@@ -1272,17 +1286,18 @@
 deltaordercmp(void *pa, void *pb)
 {
 	Meta *a, *b;
-	int cmp;
+	vlong cmp;
 
 	a = *(Meta**)pa;
 	b = *(Meta**)pb;
 	if(a->obj->type != b->obj->type)
 		return a->obj->type - b->obj->type;
-	cmp = strcmp(a->path, b->path);
+	cmp = (b->path - a->path);
 	if(cmp != 0)
-		return cmp;
-	if(a->mtime != b->mtime)
-		return a->mtime - b->mtime;
+		return (cmp < 0) ? -1 : 1;
+	cmp = a->mtime - b->mtime;
+	if(cmp != 0)
+		return (cmp < 0) ? -1 : 1;
 	return memcmp(a->obj->hash.h, b->obj->hash.h, sizeof(a->obj->hash.h));
 }
 
@@ -1305,7 +1320,7 @@
 }
 
 static void
-addmeta(Metavec *v, Objset *has, Object *o, char *path, vlong mtime)
+addmeta(Metavec *v, Objset *has, Object *o, vlong pathid, vlong mtime)
 {
 	Meta *m;
 
@@ -1316,7 +1331,7 @@
 		return;
 	m = emalloc(sizeof(Meta));
 	m->obj = o;
-	m->path = estrdup(path);
+	m->path = pathid;
 	m->mtime = mtime;
 
 	if(v->nmeta == v->metasz){
@@ -1330,7 +1345,6 @@
 freemeta(Meta *m)
 {
 	free(m->delta);
-	free(m->path);
 	free(m);
 }
 
@@ -1339,8 +1353,9 @@
 {
 	Object *t, *o;
 	Dirent *e;
+	vlong dh, eh;
+	int i, k, r;
 	char *p;
-	int i, k;
 
 	if(oshas(has, tree))
 		return 0;
@@ -1351,7 +1366,8 @@
 		unref(t);
 		return 0;
 	}
-	addmeta(v, has, t, dpath, mtime);
+	dh = murmurhash2(dpath, strlen(dpath));
+	addmeta(v, has, t, dh, mtime);
 	for(i = 0; i < t->tree->nent; i++){
 		e = &t->tree->ent[i];
 		if(oshas(has, e->h))
@@ -1360,14 +1376,16 @@
 			continue;
 		k = (e->mode & DMDIR) ? GTree : GBlob;
 		o = clearedobject(e->h, k);
-		p = smprint("%s/%s", dpath, e->name);
-		if(k == GBlob)
-			addmeta(v, has, o, p, mtime);
-		else if(loadtree(v, has, e->h, p, mtime) == -1){
+		if(k == GTree){
+			p = smprint("%s/%s", dpath, e->name);
+			r = loadtree(v, has, e->h, p, mtime);
 			free(p);
-			return -1;
+			if(r == -1)
+				return -1;
+		}else{
+			eh = murmurhash2(e->name, strlen(e->name));
+			addmeta(v, has, o, dh^eh, mtime);
 		}
-		free(p);
 	}
 	unref(t);
 	return 0;
@@ -1388,7 +1406,7 @@
 		unref(c);
 		return 0;
 	}
-	addmeta(v, has, c, "", c->commit->ctime);
+	addmeta(v, has, c, 0, c->commit->ctime);
 	r = loadtree(v, has, c->commit->tree, "", c->commit->ctime);
 	unref(c);
 	return r;
@@ -1448,7 +1466,8 @@
 
 	pct = 0;
 	dprint(1, "picking deltas\n");
-	fprint(2, "deltifying %d objects:   0%%", nmeta);
+	if(interactive)
+		fprint(2, "deltifying %d objects:   0%%", nmeta);
 	qsort(meta, nmeta, sizeof(Meta*), deltaordercmp);
 	for(i = 0; i < nmeta; i++){
 		m = meta[i];
@@ -1491,7 +1510,8 @@
 	}
 	for(i = max(0, nmeta - 10); i < nmeta; i++)
 		dtclear(&meta[i]->dtab);
-	fprint(2, "\b\b\b\b100%%\n");
+	if(interactive)
+		fprint(2, "\b\b\b\b100%%\n");
 }
 
 static int
@@ -1677,7 +1697,8 @@
 	for(i = 0; i < nmeta; i++){
 		pct = showprogress((i*100)/nmeta, pct);
 		m = meta[i];
-		if((m->off = Boffset(bfd)) == -1)
+		m->off = Boffset(bfd);
+		if(m->off == -1)
 			goto error;
 		if((o = readobject(m->obj->hash)) == nil)
 			return -1;
--- a/proto.c
+++ b/proto.c
@@ -58,8 +58,8 @@
 	char *e;
 	int n;
 
-	if(readn(c->rfd, len, 4) == -1)
-		return -1;
+	if(readn(c->rfd, len, 4) != 4)
+		sysfatal("pktline: short read from transport");
 	len[4] = 0;
 	n = strtol(len, &e, 16);
 	if(n == 0){
@@ -94,6 +94,20 @@
 }
 
 int
+fmtpkt(Conn *c, char *fmt, ...)
+{
+	char pkt[Pktmax];
+	va_list ap;
+	int n;
+
+	va_start(ap, fmt);
+	n = vsnprint(pkt, sizeof(pkt), fmt, ap);
+	n = writepkt(c, pkt, n);
+	va_end(ap);
+	return n;
+}
+
+int
 flushpkt(Conn *c)
 {
 	dprint(1, "<=w= 0000\n");
@@ -220,14 +234,27 @@
 issmarthttp(Conn *c, char *direction)
 {
 	char buf[Pktmax+1], svc[128];
-	int n;
+	int fd, n;
 
+	if((fd = webopen(c, "contenttype", OREAD)) == -1)
+		return -1;
+	n = readn(fd, buf, sizeof(buf) - 1);
+	close(fd);
+	if(n == -1)
+		return -1;
+	buf[n] = '\0';
+	snprint(svc, sizeof(svc), "application/x-git-%s-pack-advertisement", direction);
+	if(strcmp(svc, buf) != 0){
+		werrstr("dumb http protocol not supported");
+		return -1;
+	}
+
 	if((n = readpkt(c, buf, sizeof(buf))) == -1)
 		sysfatal("http read: %r");
 	buf[n] = 0;
 	snprint(svc, sizeof(svc), "# service=git-%s-pack\n", direction);
 	if(strncmp(svc, buf, n) != 0){
-		werrstr("dumb http protocol not supported");
+		werrstr("invalid initial packet line");
 		return -1;
 	}
 	if(readpkt(c, buf, sizeof(buf)) != 0){
--- a/pull
+++ b/pull
@@ -3,17 +3,13 @@
 . /sys/lib/git/common.rc
 
 fn update{
-	branch=$1
-	upstream=$2
-	url=$3
-	dir=$4
-	bflag=()
+	upstream=$1
+	url=$2
+	dir=$3
 	dflag=()
-	if(! ~ $#branch 0)
-		bflag=(-b $branch)
 	if(! ~ $#debug 0)
 		dflag='-d'
-	{git/fetch $dflag $bflag -u $upstream $url >[2=3] || die $status} | awk '
+	{git/get $dflag -u $upstream $url >[2=3] || die $status} | awk '
 	/^remote/{
 		if($2=="HEAD")
 			next
@@ -30,16 +26,11 @@
 
 gitup
 
-flagfmt='a:allbranch, b:branch branch, d:debug,
-	f:fetchonly, u:upstream upstream, q:quiet'
+flagfmt='d:debug, q:quiet, f:fetchonly,
+	u:upstream upstream'
 args=''
 eval `''{aux/getflags $*} || exec aux/usage
 
-if(~ $#branch 0)
-	branch=refs/`{git/branch}
-if(~ $allbranch 1)
-	branch=''
-
 if(~ $#upstream 0)
 	upstream=origin
 remote=`$nl{git/conf 'remote "'$upstream'".url'}
@@ -48,7 +39,7 @@
 	upstream=THEM
 }
 
-update $branch $upstream $remote
+update $upstream $remote
 if (~ $fetchonly 1)
 	exit
 
@@ -75,8 +66,7 @@
 # The remote is directly ahead of the local, and we have
 # no local commits that need merging.
 if(~ $#quiet 0)
-	git/log -s -e $local'..'$remote >[1=2]
-echo
-echo $remote':' `{git/query $local} '=>' `{git/query $remote}  >[1=2]
+	git/log -s -e $local'..'$remote
+echo $remote':' `{git/query $local} '=>' `{git/query $remote}
 git/branch -mnb $remote $local
 exit ''
--- a/query.c
+++ b/query.c
@@ -158,6 +158,7 @@
 	char query[2048], repo[512];
 
 	ARGBEGIN{
+	case 'd':	chattygit++;	break;
 	case 'p':	fullpath++;	break;
 	case 'c':	changes++;	break;
 	case 'r':	reverse ^= 1;	break;
--- a/rebase
+++ b/rebase
@@ -7,8 +7,6 @@
 eval `''{aux/getflags $*} || exec aux/usage
 
 tmp=_rebase.working
-if(! git/walk -q)
-	die dirty working tree
 if(~ $#abort 1){
 	if(! test -f .git/rebase.todo)
 		die no rebase to abort
@@ -31,7 +29,7 @@
 	src=`{git/branch}
 	dst=`{git/query $1}
 	echo $src > .git/rebase.src
-	git/log -se $dst' '$src' @ .. '$src | sed 's/^/pick /' >.git/rebase.todo
+	git/log -se $dst'..'$src | sed 's/^/pick /' >.git/rebase.todo
 	if(! ~ $#interactive 0){
 		giteditor=`{git/conf core.editor}
 		if(~ $#editor 0)
--- a/ref.c
+++ b/ref.c
@@ -5,15 +5,20 @@
 #include "git.h"
 
 typedef struct Eval	Eval;
-typedef struct XObject	XObject;
-typedef struct Objq	Objq;
 
 enum {
 	Blank,
 	Keep,
 	Drop,
+	Skip,
 };
 
+enum {
+	Lca,
+	Twixt,
+	Range,
+};
+
 struct Eval {
 	char	*str;
 	char	*p;
@@ -22,19 +27,13 @@
 	int	stksz;
 };
 
-struct XObject {
-	Object	*obj;
-	Object	*mark;
-	XObject	*queue;
-	XObject	*next;
+static char *colors[] = {
+[Keep] "keep",
+[Drop] "drop",
+[Blank] "blank",
+[Skip] "skip",
 };
 
-struct Objq {
-	Objq	*next;
-	Object	*o;
-	int	color;
-};
-
 static Object zcommit = {
 	.type=GCommit
 };
@@ -46,26 +45,6 @@
 		ev->p++;
 }
 
-int
-objdatecmp(void *pa, void *pb)
-{
-	Object *a, *b;
-	int r;
-
-	a = readobject((*(Object**)pa)->hash);
-	b = readobject((*(Object**)pb)->hash);
-	assert(a->type == GCommit && b->type == GCommit);
-	if(a->commit->mtime == b->commit->mtime)
-		r = 0;
-	else if(a->commit->mtime < b->commit->mtime)
-		r = -1;
-	else
-		r = 1;
-	unref(a);
-	unref(b);
-	return r;
-}
-
 void
 push(Eval *ev, Object *o)
 {
@@ -128,150 +107,24 @@
 	return 1;
 }
 
-XObject*
-hnode(XObject *ht[], Object *o)
-{
-	XObject *h;
-	int	hh;
-
-	hh = o->hash.h[0] & 0xff;
-	for(h = ht[hh]; h; h = h->next)
-		if(hasheq(&o->hash, &h->obj->hash))
-			return h;
-
-	h = emalloc(sizeof(*h));
-	h->obj = o;
-	h->mark = nil;
-	h->queue = nil;
-	h->next = ht[hh];
-	ht[hh] = h;
-	return h;
-}
-
-Object*
-ancestor(Object *a, Object *b)
-{
-	Object *o, *p, *r;
-	XObject *ht[256];
-	XObject *h, *q, *q1, *q2;
-	int i;
-
-	if(a == b)
-		return a;
-	if(a == nil || b == nil)
-		return nil;
-	r = nil;
-	memset(ht, 0, sizeof(ht));
-	q1 = nil;
-
-	h = hnode(ht, a);
-	h->mark = a;
-	h->queue = q1;
-	q1 = h;
-
-	h = hnode(ht, b);
-	h->mark = b;
-	h->queue = q1;
-	q1 = h;
-
-	while(1){
-		q2 = nil;
-		while(q = q1){
-			q1 = q->queue;
-			q->queue = nil;
-			o = q->obj;
-			for(i = 0; i < o->commit->nparent; i++){
-				p = readobject(o->commit->parent[i]);
-				if(p == nil)
-					goto err;
-				h = hnode(ht, p);
-				if(h->mark != nil){
-					if(h->mark != q->mark){
-						r = h->obj;
-						goto done;
-					}
-				} else {
-					h->mark = q->mark;
-					h->queue = q2;
-					q2 = h;
-				}
-			}
-		}
-		if(q2 == nil){
-err:
-			werrstr("no common ancestor");
-			break;
-		}
-		q1 = q2;
-	}
-done:
-	for(i=0; i<nelem(ht); i++){
-		while(h = ht[i]){
-			ht[i] = h->next;
-			free(h);
-		}
-	}
-	return r;
-}
-
-int
-lca(Eval *ev)
-{
-	Object *a, *b, *o;
-
-	if(ev->nstk < 2){
-		werrstr("ancestor needs 2 objects");
-		return -1;
-	}
-	a = pop(ev);
-	b = pop(ev);
-	o = ancestor(a, b);
-	if(o == nil)
-		return -1;
-	push(ev, o);
-	return 0;
-}
-
 static int
-repaint(Objset *keep, Objset *drop, Object *o)
+paint(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres, int mode)
 {
-	Object *p;
-	int i;
+	Qelt e;
+	Objq objq;
+	Objset keep, drop, skip;
+	Object *o, *c, **range;
+	int i, nskip, nrange;
 
-	if(!oshas(keep, o->hash) && !oshas(drop, o->hash)){
-		dprint(2, "repaint: blank => drop %H\n", o->hash);
-		osadd(drop, o);
-		return 0;
-	}
-	if(oshas(keep, o->hash))
-		dprint(2, "repaint: keep => drop %H\n", o->hash);
-	osadd(drop, o);
-	for(i = 0; i < o->commit->nparent; i++){
-		if((p = readobject(o->commit->parent[i])) == nil)
-			return -1;
-		if(repaint(keep, drop, p) == -1)
-			return -1;
-		unref(p);
-	}
-	return 0;
-}
-
-int
-findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres)
-{
-	Objq *q, *e, *n, **p;
-	Objset keep, drop;
-	Object *o, *c;
-	int i, ncolor;
-
-	e = nil;
-	q = nil;
-	p = &q;
 	osinit(&keep);
 	osinit(&drop);
+	osinit(&skip);
+	qinit(&objq);
+	range = nil;
+	nrange = 0;
+	nskip = 0;
+
 	for(i = 0; i < nhead; i++){
-		if(hasheq(&head[i], &Zhash))
-			continue;
 		if((o = readobject(head[i])) == nil){
 			fprint(2, "warning: %H does not point at commit\n", o->hash);
 			werrstr("read head %H: %r", head[i]);
@@ -282,17 +135,11 @@
 			unref(o);
 			continue;
 		}
-		dprint(1, "twixt init: keep %H\n", o->hash);
-		e = emalloc(sizeof(Objq));
-		e->o = o;
-		e->color = Keep;
-		*p = e;
-		p = &e->next;
+		dprint(1, "init: keep %H\n", o->hash);
+		qput(&objq, o, Keep);
 		unref(o);
 	}		
 	for(i = 0; i < ntail; i++){
-		if(hasheq(&tail[i], &Zhash))
-			continue;
 		if((o = readobject(tail[i])) == nil){
 			werrstr("read tail %H: %r", tail[i]);
 			return -1;
@@ -303,77 +150,151 @@
 			continue;
 		}
 		dprint(1, "init: drop %H\n", o->hash);
-		e = emalloc(sizeof(Objq));
-		e->o = o;
-		e->color = Drop;
-		*p = e;
-		p = &e->next;
+		qput(&objq, o, Drop);
 		unref(o);
 	}
 
 	dprint(1, "finding twixt commits\n");
-	while(q != nil){
-		if(oshas(&drop, q->o->hash))
-			ncolor = Drop;
-		else if(oshas(&keep, q->o->hash))
-			ncolor = Keep;
-		else
-			ncolor = Blank;
-		if(ncolor == Drop || ncolor == Keep && q->color == Keep)
-			goto next;
-		if(ncolor == Keep && q->color == Drop){
-			if(repaint(&keep, &drop, q->o) == -1)
+	while(nskip != objq.nheap && qpop(&objq, &e)){
+		if(e.color == Skip)
+			nskip--;
+		if(oshas(&skip, e.o->hash))
+			continue;
+		switch(e.color){
+		case Keep:
+			if(oshas(&keep, e.o->hash))
+				continue;
+			if(oshas(&drop, e.o->hash))
+				e.color = Skip;
+			else if(mode == Range){
+				range = earealloc(range, nrange+1, sizeof(Object*));
+				range[nrange++] = e.o;
+			}
+			osadd(&keep, e.o);
+			break;
+		case Drop:
+			if(oshas(&drop, e.o->hash))
+				continue;
+			if(oshas(&keep, e.o->hash))
+				e.color = Skip;
+			osadd(&drop, e.o);
+			break;
+		case Skip:
+			osadd(&skip, e.o);
+			break;
+		}
+		o = readobject(e.o->hash);
+		for(i = 0; i < o->commit->nparent; i++){
+			if((c = readobject(e.o->commit->parent[i])) == nil)
 				goto error;
-		}else if (ncolor == Blank) {
-			dprint(2, "visit: %s %H\n", q->color == Keep ? "keep" : "drop", q->o->hash);
-			if(q->color == Keep)
-				osadd(&keep, q->o);
-			else
-				osadd(&drop, q->o);
-			for(i = 0; i < q->o->commit->nparent; i++){
-				if((c = readobject(q->o->commit->parent[i])) == nil)
-					goto error;
-				if(c->type != GCommit){
-					fprint(2, "warning: %H does not point at commit\n", c->hash);
-					unref(c);
-					continue;
-				}
-				dprint(2, "enqueue: %s %H\n", q->color == Keep ? "keep" : "drop", c->hash);
-				n = emalloc(sizeof(Objq));
-				n->color = q->color;
-				n->next = nil;
-				n->o = c;
-				e->next = n;
-				e = n;
+			if(c->type != GCommit){
+				fprint(2, "warning: %H does not point at commit\n", c->hash);
 				unref(c);
+				continue;
 			}
-		}else{
-			sysfatal("oops");
+			dprint(2, "\tenqueue: %s %H\n", colors[e.color], c->hash);
+			qput(&objq, c, e.color);
+			unref(c);
+			if(e.color == Skip)
+				nskip++;
 		}
-next:
-		n = q->next;
-		free(q);
-		q = n;
+		unref(o);
 	}
-	*res = eamalloc(keep.nobj, sizeof(Object*));
-	*nres = 0;
-	for(i = 0; i < keep.sz; i++){
-		if(keep.obj[i] != nil && !oshas(&drop, keep.obj[i]->hash)){
-			(*res)[*nres] = keep.obj[i];
-			(*nres)++;
+	switch(mode){
+	case Lca:
+		dprint(1, "found ancestor\n");
+		o = nil;
+		for(i = 0; i < keep.sz; i++){
+			o = keep.obj[i];
+			if(o != nil && oshas(&drop, o->hash) && !oshas(&skip, o->hash))
+				break;
 		}
+		if(i == keep.sz){
+			*nres = 0;
+			*res = nil;
+		}else{
+			*nres = 1;
+			*res = eamalloc(1, sizeof(Object*));
+			(*res)[0] = o;
+		}
+		break;
+	case Twixt:
+		dprint(1, "found twixt\n");
+		*res = eamalloc(keep.nobj, sizeof(Object*));
+		*nres = 0;
+		for(i = 0; i < keep.sz; i++){
+			o = keep.obj[i];
+			if(o != nil && !oshas(&drop, o->hash) && !oshas(&skip, o->hash)){
+				(*res)[*nres] = o;
+				(*nres)++;
+			}
+		}
+		break;
+	case Range:
+		dprint(1, "found range\n");
+		*res = eamalloc(nrange, sizeof(Object*));
+		*nres = 0;
+		for(i = nrange - 1; i >= 0; i--){
+			o = range[i];
+			if(!oshas(&drop, o->hash) && !oshas(&skip, o->hash)){
+				(*res)[*nres] = o;
+				(*nres)++;
+			}
+		}
+		free(range);
+		break;
 	}
 	osclear(&keep);
 	osclear(&drop);
+	osclear(&skip);
 	return 0;
 error:
-	for(; q != nil; q = n) {
-		n = q->next;
-		free(q);
-	}
+	dprint(1, "paint error: %r\n");
+	free(objq.heap);
+	free(range);
 	return -1;
 }
 
+int
+findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres)
+{
+	return paint(head, nhead, tail, ntail, res, nres, Twixt);
+}
+
+Object*
+ancestor(Object *a, Object *b)
+{
+	Object **o, *r;
+	int n;
+
+	if(paint(&a->hash, 1, &b->hash, 1, &o, &n, Lca) == -1 || n == 0)
+		return nil;
+	r = ref(o[0]);
+	free(o);
+	return r;
+}
+
+int
+lca(Eval *ev)
+{
+	Object *a, *b, **o;
+	int n;
+
+	if(ev->nstk < 2){
+		werrstr("ancestor needs 2 objects");
+		return -1;
+	}
+	n = 0;
+	b = pop(ev);
+	a = pop(ev);
+	paint(&a->hash, 1, &b->hash, 1, &o, &n, Lca);
+	if(n == 0)
+		return -1;
+	push(ev, *o);
+	free(o);
+	return 0;
+}
+
 static int
 parent(Eval *ev)
 {
@@ -393,33 +314,10 @@
 }
 
 static int
-unwind(Eval *ev, Object **obj, int *idx, int nobj, Object **p, Objset *set, int keep)
-{
-	int i;
-
-	for(i = nobj; i >= 0; i--){
-		idx[i]++;
-		if(keep && !oshas(set, obj[i]->hash)){
-			push(ev, obj[i]);
-			osadd(set, obj[i]);
-		}else{
-			osadd(set, obj[i]);
-		}
-		if(idx[i] < obj[i]->commit->nparent){
-			*p = obj[i];
-			return i;
-		}
-		unref(obj[i]);
-	}
-	return -1;
-}
-
-static int
 range(Eval *ev)
 {
-	Object *a, *b, *p, *q, **all;
-	int nall, *idx, mark;
-	Objset keep, skip;
+	Object *a, *b, **o;
+	int i, n;
 
 	b = pop(ev);
 	a = pop(ev);
@@ -432,50 +330,12 @@
 		return -1;
 	}
 
-	p = b;
-	all = nil;
-	idx = nil;
-	nall = 0;
-	mark = ev->nstk;
-	osinit(&keep);
-	osinit(&skip);
-	osadd(&keep, a);
-	while(1){
-		all = earealloc(all, (nall + 1), sizeof(Object*));
-		idx = earealloc(idx, (nall + 1), sizeof(int));
-		all[nall] = p;
-		idx[nall] = 0;
-		if(p == a || p->commit->nparent == 0 && a == &zcommit){
-			if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1)
-				break;
-		}else if(p->commit->nparent == 0){
-			if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1)
-				break;
-		}else if(oshas(&keep, p->hash)){
-			if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1)
-				break;
-		}else if(oshas(&skip, p->hash))
-			if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1)
-				break;
-		if(p->commit->nparent == 0)
-			break;
-		if((q = readobject(p->commit->parent[idx[nall]])) == nil){
-			werrstr("bad commit %H", p->commit->parent[idx[nall]]);
-			goto error;
-		}
-		if(q->type != GCommit){
-			werrstr("not commit: %H", q->hash);
-			goto error;
-		}
-		p = q;
-		nall++;
-	}
-	free(all);
-	qsort(ev->stk + mark, ev->nstk - mark, sizeof(Object*), objdatecmp);
+	if(paint(&b->hash, 1, &a->hash, 1, &o, &n, Range) == -1)
+		return -1;
+	for(i = 0; i < n; i++)
+		push(ev, o[i]);
+	free(o);
 	return 0;
-error:
-	free(all);
-	return -1;
 }
 
 int
--- a/revert
+++ b/revert
@@ -5,16 +5,18 @@
 gitup
 
 flagfmt='c:query query' args='file ...'
-eval `''{aux/getflags $*} || exec aux/usage
+if (! eval `''{aux/getflags $*} || ~ $#* 0)
+	exec aux/usage
 
 commit=$gitfs/HEAD
 if(~ $#query 1)
 	commit=`{git/query -p $query}
 
-files=`$nl{cleanname -d $gitrel $*}
-for(f in `$nl{cd $commit/tree/ && walk -f $files}){
+files=`$nl{cleanname -d $gitrel $* | drop $gitroot}
+for(f in `$nl{cd $commit/tree/ && walk -f ./$files}){
 	mkdir -p `{basename -d $f}
 	cp -x -- $commit/tree/$f $f
+	touch $f
 	git/add $f
 }
 exit ''
--- a/save.c
+++ b/save.c
@@ -14,17 +14,27 @@
 	Maxparents = 16,
 };
 
+char	*authorname;
+char	*authoremail;
+char	*committername;
+char	*committeremail;
+char	*commitmsg;
+Hash	parents[Maxparents];
+int	nparents;
+
 int
-gitmode(int m)
+gitmode(Dirent *e)
 {
-	if(m & DMDIR)		/* directory */
+	if(e->islink)
+		return 0120000;
+	else if(e->ismod)
+		return 0160000;
+	else if(e->mode & DMDIR)
 		return 0040000;
-	else if(m & 0111)	/* executable */
+	else if(e->mode & 0111)
 		return 0100755;
-	else if(m != 0)		/* regular */
+	else
 		return 0100644;
-	else			/* symlink */
-		return 0120000;
 }
 
 int
@@ -141,7 +151,7 @@
 	for(d = ent; d != ent + nent; d++){
 		if(strlen(d->name) >= 255)
 			sysfatal("overly long filename: %s", d->name);
-		t = seprint(t, etxt, "%o %s", gitmode(d->mode), d->name) + 1;
+		t = seprint(t, etxt, "%o %s", gitmode(d), d->name) + 1;
 		memcpy(t, d->h.h, sizeof(d->h.h));
 		t += sizeof(d->h.h);
 	}
@@ -297,7 +307,7 @@
 
 
 void
-mkcommit(Hash *c, char *msg, char *name, char *email, vlong date, Hash *parents, int nparents, Hash tree)
+mkcommit(Hash *c, vlong date, Hash tree)
 {
 	char *s, h[64];
 	int ns, nh, i;
@@ -307,10 +317,10 @@
 	fmtprint(&f, "tree %H\n", tree);
 	for(i = 0; i < nparents; i++)
 		fmtprint(&f, "parent %H\n", parents[i]);
-	fmtprint(&f, "author %s <%s> %lld +0000\n", name, email, date);
-	fmtprint(&f, "committer %s <%s> %lld +0000\n", name, email, date);
+	fmtprint(&f, "author %s <%s> %lld +0000\n", authorname, authoremail, date);
+	fmtprint(&f, "committer %s <%s> %lld +0000\n", committername, committeremail, date);
 	fmtprint(&f, "\n");
-	fmtprint(&f, "%s", msg);
+	fmtprint(&f, "%s", commitmsg);
 	s = fmtstrflush(&f);
 
 	ns = strlen(s);
@@ -344,9 +354,9 @@
 void
 main(int argc, char **argv)
 {
-	Hash th, ch, parents[Maxparents];
-	char *msg, *name, *email, *dstr, cwd[1024];
-	int i, r, ncwd, nparents;
+	Hash th, ch;
+	char *dstr, cwd[1024];
+	int i, r, ncwd;
 	vlong date;
 	Object *t;
 
@@ -355,19 +365,29 @@
 		sysfatal("could not find git repo: %r");
 	if(getwd(cwd, sizeof(cwd)) == nil)
 		sysfatal("getcwd: %r");
-	msg = nil;
-	name = nil;
-	email = nil;
 	dstr = nil;
 	date = time(nil);
-	nparents = 0;
 	ncwd = strlen(cwd);
 
 	ARGBEGIN{
-	case 'm':	msg = EARGF(usage());	break;
-	case 'n':	name = EARGF(usage());	break;
-	case 'e':	email = EARGF(usage());	break;
-	case 'd':	dstr = EARGF(usage());	break;
+	case 'm':
+		commitmsg = EARGF(usage());
+		break;
+	case 'n':
+		authorname = EARGF(usage());
+		break;
+	case 'e':
+		authoremail = EARGF(usage());
+		break;
+	case 'N':
+		committername = EARGF(usage());
+		break;
+	case 'E':
+		committeremail = EARGF(usage());
+		break;
+	case 'd':
+		dstr = EARGF(usage());
+		break;
 	case 'p':
 		if(nparents >= Maxparents)
 			sysfatal("too many parents");
@@ -376,21 +396,26 @@
 		break;
 	default:
 		usage();
+		break;
 	}ARGEND;
 
-	if(!msg)
+	if(commitmsg == nil)
 		sysfatal("missing message");
-	if(!name)
+	if(authorname == nil)
 		sysfatal("missing name");
-	if(!email)
+	if(authoremail == nil)
 		sysfatal("missing email");
+	if((committername == nil) != (committeremail == nil))
+		sysfatal("partially specified committer");
+	if(committername == nil && committeremail == nil){
+		committername = authorname;
+		committeremail = authoremail;
+	}
 	if(dstr){
 		date=strtoll(dstr, &dstr, 10);
 		if(strlen(dstr) != 0)
 			sysfatal("could not parse date %s", dstr);
 	}
-	if(msg == nil || name == nil)
-		usage();
 	for(i = 0; i < argc; i++){
 		cleanname(argv[i]);
 		if(*argv[i] == '/' && strncmp(argv[i], cwd, ncwd) == 0)
@@ -403,7 +428,7 @@
 	r = treeify(t, argv, argv + argc, 0, &th);
 	if(r == -1)
 		sysfatal("could not commit: %r\n");
-	mkcommit(&ch, msg, name, email, date, parents, nparents, th);
+	mkcommit(&ch, date, th);
 	print("%H\n", ch);
 	exits(nil);
 }
--- a/send.c
+++ b/send.c
@@ -134,8 +134,8 @@
 	nmap = nours;
 	map = eamalloc(nmap, sizeof(Map));
 	for(i = 0; i < nmap; i++){
-		map[i].ours = ours[i];
 		map[i].theirs = Zhash;
+		map[i].ours = ours[i];
 		map[i].ref = refs[i];
 	}
 	while(1){
@@ -155,9 +155,15 @@
 		theirs = earealloc(theirs, ntheirs+1, sizeof(Hash));
 		if(hparse(&theirs[ntheirs], sp[0]) == -1)
 			sysfatal("invalid hash %s", sp[0]);
+		if((idx = findkey(map, nmap, sp[1])) != -1)
+			map[idx].theirs = theirs[ntheirs];
+		/*
+		 * we only keep their ref if we can read the object to add it
+		 * to our reachability; otherwise, discard it; we only care
+		 * that we don't have it, so we can tell whether we need to
+		 * bail out of pushing.
+		 */
 		if((o = readobject(theirs[ntheirs])) != nil){
-			if((idx = findkey(map, nmap, sp[1])) != -1)
-				map[idx].theirs = theirs[ntheirs];
 			ntheirs++;
 			unref(o);
 		}
@@ -180,7 +186,7 @@
 			p = ancestor(a, b);
 		if(!force && !hasheq(&m->theirs, &Zhash) && (a == nil || p != a)){
 			fprint(2, "remote has diverged\n");
-			werrstr("force needed");
+			werrstr("remote diverged");
 			flushpkt(c);
 			return -1;
 		}
--- a/serve.c
+++ b/serve.c
@@ -9,20 +9,6 @@
 int	allowwrite;
 
 int
-fmtpkt(Conn *c, char *fmt, ...)
-{
-	char pkt[Pktmax];
-	va_list ap;
-	int n;
-
-	va_start(ap, fmt);
-	n = vsnprint(pkt, sizeof(pkt), fmt, ap);
-	n = writepkt(c, pkt, n);
-	va_end(ap);
-	return n;
-}
-
-int
 showrefs(Conn *c)
 {
 	int i, ret, nrefs;
@@ -34,7 +20,7 @@
 	refs = nil;
 	names = nil;
 	if(resolveref(&head, "HEAD") != -1)
-		if(fmtpkt(c, "%H HEAD", head) == -1)
+		if(fmtpkt(c, "%H HEAD\n", head) == -1)
 			goto error;
 
 	if((nrefs = listrefs(&refs, &names)) == -1)
@@ -362,7 +348,7 @@
 int
 updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd)
 {
-	char refpath[512];
+	char refpath[512], buf[128];
 	int i, newidx, hadref, fd, ret, lockfd;
 	vlong newtm;
 	Object *o;
@@ -378,7 +364,7 @@
 	 */
 	newtm = -23811206400;	
 	if((lockfd = lockrepo()) == -1){
-		werrstr("repo locked\n");
+		snprint(buf, sizeof(buf), "repo locked\n");
 		return -1;
 	}
 	for(i = 0; i < nupd; i++){
@@ -385,12 +371,12 @@
 		if(resolveref(&h, ref[i]) == 0){
 			hadref = 1;
 			if(!hasheq(&h, &cur[i])){
-				werrstr("old ref changed: %s", ref[i]);
+				snprint(buf, sizeof(buf), "old ref changed: %s", ref[i]);
 				goto error;
 			}
 		}
 		if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){
-			werrstr("ref path too long: %s", ref[i]);
+			snprint(buf, sizeof(buf), "ref path too long: %s", ref[i]);
 			goto error;
 		}
 		if(hasheq(&upd[i], &Zhash)){
@@ -398,11 +384,11 @@
 			continue;
 		}
 		if((o = readobject(upd[i])) == nil){
-			werrstr("update to nonexistent hash %H", upd[i]);
+			snprint(buf, sizeof(buf), "update to nonexistent hash %H", upd[i]);
 			goto error;
 		}
 		if(o->type != GCommit){
-			werrstr("not commit: %H", upd[i]);
+			snprint(buf, sizeof(buf), "not commit: %H", upd[i]);
 			goto error;
 		}
 		if(o->commit->mtime > newtm){
@@ -411,11 +397,11 @@
 		}
 		unref(o);
 		if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){
-			werrstr("open ref: %r");
+			snprint(buf, sizeof(buf), "open ref: %r");
 			goto error;
 		}
 		if(fprint(fd, "%H", upd[i]) == -1){
-			werrstr("upate ref: %r");
+			snprint(buf, sizeof(buf), "upate ref: %r");
 			close(fd);
 			goto error;
 		}
@@ -436,11 +422,11 @@
 	 */
 	if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){
 		if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){
-			werrstr("open HEAD: %r");
+			snprint(buf, sizeof(buf), "open HEAD: %r");
 			goto error;
 		}
 		if(fprint(fd, "ref: %s", ref[0]) == -1){
-			werrstr("write HEAD ref: %r");
+			snprint(buf, sizeof(buf), "write HEAD ref: %r");
 			goto error;
 		}
 		close(fd);
@@ -447,8 +433,9 @@
 	}
 	ret = 0;
 error:
-	fmtpkt(c, "ERR %r");
+	fmtpkt(c, "ERR %s", buf);
 	close(lockfd);
+	werrstr(buf);
 	return ret;
 }
 
--- a/util.c
+++ b/util.c
@@ -10,6 +10,10 @@
 int chattygit;
 int interactive = 1;
 
+enum {
+	Seed		= 2928213749ULL
+};
+
 Object*
 emptydir(void)
 {
@@ -320,4 +324,121 @@
 		fprint(2, "\b\b\b\b%3d%%", pct);
 	}
 	return pct;
+}
+
+void
+qinit(Objq *q)
+{
+	memset(q, 0, sizeof(Objq));
+	q->nheap = 0;
+	q->heapsz = 8;
+	q->heap = eamalloc(q->heapsz, sizeof(Qelt));
+}
+
+void
+qclear(Objq *q)
+{
+	free(q->heap);
+}
+
+void
+qput(Objq *q, Object *o, int color)
+{
+	Qelt t;
+	int i;
+
+	if(q->nheap == q->heapsz){
+		q->heapsz *= 2;
+		q->heap = earealloc(q->heap, q->heapsz, sizeof(Qelt));
+	}
+	q->heap[q->nheap].o = o;
+	q->heap[q->nheap].color = color;
+	q->heap[q->nheap].ctime = o->commit->ctime;
+	for(i = q->nheap; i > 0; i = (i-1)/2){
+		if(q->heap[i].ctime < q->heap[(i-1)/2].ctime)
+			break;
+		t = q->heap[i];
+		q->heap[i] = q->heap[(i-1)/2];
+		q->heap[(i-1)/2] = t;
+	}
+	q->nheap++;
+}
+
+int
+qpop(Objq *q, Qelt *e)
+{
+	int i, l, r, m;
+	Qelt t;
+
+	if(q->nheap == 0)
+		return 0;
+	*e = q->heap[0];
+	if(--q->nheap == 0)
+		return 1;
+
+	i = 0;
+	q->heap[0] = q->heap[q->nheap];
+	while(1){
+		m = i;
+		l = 2*i+1;
+		r = 2*i+2;
+		if(l < q->nheap && q->heap[m].ctime < q->heap[l].ctime)
+			m = l;
+		if(r < q->nheap && q->heap[m].ctime < q->heap[r].ctime)
+			m = r;
+		if(m == i)
+			break;
+		t = q->heap[m];
+		q->heap[m] = q->heap[i];
+		q->heap[i] = t;
+		i = m;
+	}
+	return 1;
+}
+
+u64int
+murmurhash2(void *pp, usize n)
+{
+	u32int m = 0x5bd1e995;
+	u32int r = 24;
+	u32int h, k;
+	u32int *w, *e;
+	uchar *p;
+	
+	h = Seed ^ n;
+	e = pp;
+	e += (n / 4);
+	for (w = pp; w != e; w++) {
+		/*
+		 * NB: this is endian dependent.
+		 * This is fine for use in git, since the
+		 * hashes computed here are only ever used
+		 * for in memory data structures.
+		 *
+		 * Pack files will differ when packed on
+		 * machines with different endianness,
+		 * but the results will still be correct.
+		 */
+		k = *w;
+		k *= m;
+		k ^= k >> r;
+		k *= m;
+
+		h *= m;
+		h ^= k;
+	}
+
+	p = (uchar*)w;
+	switch (n & 0x3) {
+	case 3:	h ^= p[2] << 16;
+	case 2:	h ^= p[1] << 8;
+	case 1:	h ^= p[0] << 0;
+		h *= m;
+	}
+
+	h ^= h >> 13;
+	h *= m;
+	h ^= h >> 15;
+
+	return h;
 }
--- a/walk.c
+++ b/walk.c
@@ -295,10 +295,12 @@
 			if(!quiet && (printflg & Tflg))
 				print("%s%s\n", tstr, p);
 		}else{
-			if(d == nil || access(rpath, AEXIST) == 0){
-				dirty |= Rflg;
-				if(!quiet && (printflg & Rflg))
-					print("%s%s\n", rstr, p);
+			if(d == nil || access(rpath, AEXIST) == 0 ){
+				if(access(bpath, AEXIST) == 0){
+					dirty |= Rflg;
+					if(!quiet && (printflg & Rflg))
+						print("%s%s\n", rstr, p);
+				}
 			}else if(access(bpath, AEXIST) == -1) {
 				dirty |= Aflg;
 				if(!quiet && (printflg & Aflg))