shithub: git9

Download patch

ref: ec28e68d5f5d72748d4b2d0be2861956b856ef4f
author: Ori Bernstein <ori@eigenstate.org>
date: Fri Jun 28 22:18:37 EDT 2019

Import git9 from mercurial repository

--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,19 @@
+Copyright (c) 2019 Ori Bernstein
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+++ b/README
@@ -1,0 +1,151 @@
+Git for Plan 9: git/fs
+======================
+
+Plan 9 is a non-posix system that currently has no git port.  Git
+itself feels distinctly un-plan9ish.
+
+Git/fs implements a git client for plan 9.  The intent is to support
+working with git repositories, without cloning the git interface
+directly.
+
+Git/fs is alpha software. It more or less works for me, but has many
+bugs, and many more missing tools, but it's at the point where the
+hard parts are done, and "the rest is just scripting", using standard
+command line tools (cp, echo, diff, patch, and diff3).
+
+Structure
+---------
+
+The git/fs program provides a file system mounted on /mnt/git.  It
+provides a read-only view into the repository contents to allow
+scripts to inspect the data.  Surrounding scripts and binaries will
+manipulate the repository contents directly.  These changes will be
+immediately mirrored in the file system.
+
+Scripts will generally mount git/fs as needed to do
+their work, but if you want to browse the repository
+manually, run it yourself. You'll get `/mnt/git` mounted,
+with the following contents:
+
+	/mnt/git/object:	The objects in the repo.
+	/mnt/git/branch:	The branches in the repo.
+	/mnt/git/ctl:		A file showing the status of the repo.
+				Currently, it only shows the current branch.
+	/mnt/git/HEAD		An alias for the currently checked out
+				commit directory.
+
+Visible Differences
+-------------------
+
+The most obvious difference is that Git's index is a bit boneheaded, so I'm
+ignoring it.  The index doesn't affect the wire protocol, so this
+isn't an interoperability issue, unless you share the same physical
+repository on both Plan 9 and Unix.  If you do, expect them to disagree
+about the files that have been modified in the working copy.
+
+In fact, the entire concept of the staging area has been dropped, as
+it's both confusing and clunky.  There are now only three states that
+files can be in: 'untracked', 'dirty', and 'committed'.  Tracking is
+done with empty files under .git/index9/{removed,tracked}/path/to/file.
+
+It's implemented in Plan 9 flavor C, and provides tools for writing
+repository contents, and a file system for read-only access, which
+will mirror the current state of the repository.
+
+Installation
+------------
+
+Install with `mk install`.
+
+Examples
+--------
+
+Some usage examples:
+
+	git/clone git://git.eigenstate.org/ori/mc.git
+	git/log
+	cd subdir/name
+	git/add foo.c
+	diff bar.c /mnt/git/HEAD/
+	git/commit
+	git/push
+
+Commits are presented as directories with the following
+contents:
+
+	author:	A file containing the author name
+	hash:	A file containing the commit hash
+	parent:	A file containing the commit parents, one per line.
+	msg:	A file containing the log message for that commit
+	tree:	A directory containing a view of the repository.
+
+So, for example:
+
+	% ls /mnt/git/branch/heads/master
+	/mnt/git/branch/heads/master/author
+	/mnt/git/branch/heads/master/hash
+	/mnt/git/branch/heads/master/msg
+	/mnt/git/branch/heads/master/parent
+	/mnt/git/branch/heads/master/tree
+	% cat /mnt/git/branch/heads/master/hash
+	7d539a7c08aba3f31b3913e0efef11c43ea9
+
+	# This is the same commit, with the same contents.
+	% ls /mnt/git/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef
+	/mnt/git/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/author
+	/mnt/git/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/hash
+	/mnt/git/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/msg
+	/mnt/git/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/parent
+	/mnt/git/object/7d539a7c08aba3f31b3913e0efef11c43ea9f9ef/tree
+
+	# what git/diff will hopefully do more concisely soon, filtering
+	# out the non-git files.
+	ape/diff -ur /mnt/git/branch/heads/master/tree .
+	Only in .: .git
+	Only in .: debug
+	diff -ur /mnt/git/branch/heads/master/tree/fold.myr ./fold.myr
+	--- /mnt/git/branch/heads/master/tree/fold.myr	Wed Dec 31 16:00:00 1969
+	+++ ./fold.myr	Mon Apr  1 21:39:06 2019
+	@@ -6,6 +6,8 @@
+	 	const foldexpr : (e : expr# -> std.option(constval))
+	 ;;
+	 
+	+/* Look, diffing files just works, and I don't need any fancy glue! */
+	+
+	 const foldexpr = {e
+	 	match e
+	 	| &(`Eident &[.sc=`Sclassenum, .name=name, .ty=`Tyenum &(`Body enum)]):
+	Only in .: refs
+
+	
+The following utilities and binaries are provided:
+
+	fs:	The git filesystem.
+	fetch:	The protocol bits for getting data from a git server.
+	send:	The protocol bits for sending data to a git server.
+	save:	The gnarly bits for storing the files for a commit.
+	conf:	A program to extract information from a config file.
+	clone:	Clones a repository.
+	commit:	Commits a snapshot of the working directory.
+	log:	Prints the contents of a commmit log.
+	add:	Tells the repository to add a file to the next commit.
+	walk:	`du`, but for git status.
+
+
+Supported protocols: git:// and git+ssh://. If someone
+implements others, I'll gladly accept patches.
+
+TODOs
+-----
+
+Documentation has not yet been written.  You'll need to read the
+source. Notably missing functionality includes:
+
+	git/mkpatch:	Generate a 'git am' compatible patch.
+	git/apply:	Apply a diff.
+	git/diff:	Wrapper wrapper around git/walk that
+			diffs the changed files.
+	git/merge:	Yup, what it says on the label. Should
+			also be a script around git/fs.
+	git/log:	Need to figure out how to make it filter
+			by files.
--- /dev/null
+++ b/add
@@ -1,0 +1,46 @@
+#!/bin/rc -e
+
+rfork ne
+
+fn usage {
+	echo 'usage: '$argv0' [-r] files..' >[1=2]
+	echo '	-r:		remove instead of adding' >[1=2]
+}
+
+add='tracked'
+del='removed'
+while(~ $1 -*){
+	switch($1){
+	case -r
+		add='removed'
+		del='tracked'
+	case --
+		break
+	case *; usage
+	}
+	shift
+}
+
+dir=`{pwd}
+base=`{git/conf -r}
+if(! ~ $status ''){
+	echo 'not in git repository' `{pwd} >[1=2]
+	exit notrepo
+}
+
+cd $base
+rel=`{sed 's@^'$base'/*@@' <{echo $dir}}
+if(~ $#rel 0)
+	rel=''
+for(f in $*){
+	if(! test -f $base/$rel/$f){
+		echo 'could not add '$base/$rel/$f': does not exist' >[1=2]
+		exit 'nofile'
+	}
+	addpath=.git/index9/$add/$rel/$f
+	delpath=.git/index9/$del/$rel/$f
+	mkdir -p `{basename -d $addpath}
+	mkdir -p `{basename -d $delpath}
+	touch $addpath
+	rm -f $delpath
+}
--- /dev/null
+++ b/branch
@@ -1,0 +1,81 @@
+#!/bin/rc -e
+
+rfork en
+
+fn usage{
+	echo usage: $argv0 [-b base] [-o origin] new >[2=1]
+	echo '	'-b base:	use "base" for branch (default: current branch) >[2=1]
+	echo '	'-o origin:	use "origin" for remote branch >[2=1]
+	echo '	'new:		name of new branch
+	exit usage
+}
+
+if(! cd `{git/conf -r}){
+	exit 'not in git repository'
+	exit notgit
+}
+git/fs
+
+nl='
+'
+stay=''
+create=''
+cur=`{awk '$1=="branch"{print $2}' < /mnt/git/ctl}
+while(~ $1 -* && ! ~ $1 --){
+	switch($1){
+	case -c; create=true
+	case -s; stay=true
+	case -o; origin=$1
+	case *; usage
+	}
+	shift
+}
+if(~ $1 --) shift
+
+if(~ $#* 0){
+	echo $cur
+	exit
+}
+if(! ~ $#* 1)
+	usage
+new=$1
+
+if(~ $create ''){
+	if(! test -e .git/refs/heads/$new){
+		echo branch $new: does not exist >[1=2]
+		exit exists
+	}
+}
+if not{
+	if(test -e .git/refs/heads/$new){
+		echo could not create $new: already exists >[1=2]
+		exit exists
+	}
+	branched=''
+	candidates=(.git/refs/$cur .git/refs/heads/$cur .git/refs/remotes/$cur .git/refs/remotes/*/$cur)
+	for(br in $candidates){
+		if(test -f $br){
+			echo 'creating new branch '$new
+			cp $br .git/refs/heads/$new
+			branched="ok"
+		}
+	}
+	if(~ $branched ''){
+		echo 'could not find branch '$cur >[1=2]
+		exit notfound
+	}
+}
+
+if(~ $stay ''){
+	rm -f `$nl{git/walk -cfT}
+	echo 'ref: refs/heads/'$new > .git/HEAD
+	tree=/mnt/git/HEAD/tree
+	@{builtin cd $tree && tar cif /fd/1 .} | @{tar xf /fd/0}
+	for(f in `$nl{walk -f $tree | sed 's@^'$tree'/*@@'}){
+		if(! ~ $#f 0){
+			idx=.git/index9/tracked/$f
+			mkdir -p `{basename -d $idx}
+			walk -eq $f > $idx
+		}
+	}
+}
--- /dev/null
+++ b/clone
@@ -1,0 +1,98 @@
+#!/bin/rc
+
+rfork en
+nl='
+'
+
+if(~ $#* 1){
+	remote=$1
+	local=`{basename $1 .git}
+}
+if not if(~ $#* 2){
+	remote=$1
+	local=$2
+}
+if not{
+	echo usage: git/clone remote [local] >[1=2]
+	exit usage
+}
+
+if(test -e $local){
+	echo $local already exists
+	exit exists
+}
+	
+fn clone{
+	mkdir -p $local/.git
+	mkdir -p $local/.git/objects/pack/
+	mkdir -p $local/.git/refs/heads/
+	
+	cd $local
+	
+	dircp /sys/lib/git/template .git
+	echo '[remote "origin"]' 				>> .git/config
+	echo '	url='$remote					>> .git/config
+	echo '	fetch=+refs/heads/*:refs/remotes/origin/*'	>> .git/config
+	{git/fetch $remote >[2=3] | awk '
+		/^remote/{
+			if($2=="HEAD"){
+				headhash=$3
+				headref=""
+			}else{
+				gsub("^refs/heads", "refs/remotes/origin", $2)
+				if($2 == "refs/remotes/origin/master" || $3 == headhash)
+					headref=$2
+				outfile = ".git/" $2
+				system("mkdir -p `{basename -d "outfile"}")
+				print $3 > outfile
+				close(outfile)
+			}
+		}
+		END{
+			if(headref != ""){
+				remote = headref
+				gsub("^refs/remotes/origin", "refs/heads", headref)
+				system("mkdir -p `{basename -d .git/" headref"}");
+				system("cp .git/" remote " .git/" headref)
+				print "ref: " headref > ".git/HEAD"
+			}else{
+				print "warning: detached head "headhash > "/fd/2"
+				print headhash > ".git/HEAD"
+			}
+		}
+	'} |[3] tr '\x0d' '\x0a'
+	if(! ~ $status '|')
+		exit 'clone:' $status
+
+	tree=/mnt/git/branch/heads/master/tree
+	echo checking out repository...
+	if(test -f .git/refs/remotes/origin/master){
+		cp .git/refs/remotes/origin/master .git/refs/heads/master
+		git/fs
+		@ {builtin cd $tree && tar cif /fd/1 .} | @ {tar xf /fd/0}
+		if(! ~ $status '')
+			exit 'checkout:' $status
+		for(f in `$nl{walk -f $tree | sed 's@^'$tree'/*@@'}){
+			if(! ~ $#f 0){
+				idx=.git/index9/tracked/$f
+				mkdir -p `{basename -d $idx}
+				walk -eq $f > $idx
+			}
+		}
+	}
+	if not{
+		echo no master branch >[1=2]
+		echo check out your code with git/branch >[1=2]
+	}
+}
+
+@{clone}
+st=$status
+if(~ $st ''){
+	echo done.
+}
+if not{
+	echo clone failed: $st >[2=1]
+	echo cleaning up $local >[2=1]
+	rm -rf $local
+}
--- /dev/null
+++ b/commit
@@ -1,0 +1,119 @@
+#!/bin/rc
+
+rfork ne
+
+nl='
+'
+
+fn whoami{
+	name=`{git/conf user.name}
+	email=`{git/conf user.email}
+	msgfile=.git/git-msg.$pid
+	if(test -f /adm/keys.who){
+		if(~ $name '')
+			name=`{cat /adm/keys.who | awk -F'|' '$1=="'$user'" {print $3}'}
+		if(~ $email '')
+			email=`{cat /adm/keys.who | awk -F'|' '$1=="'$user'" {print $5}'}
+	}
+	if(~ $name '')
+		name=glenda
+	if(~ $email '')
+		email=glenda@9front.local
+}
+
+fn findbranch{
+	branch=`{awk '$1=="branch"{print $2}' < /mnt/git/ctl}
+	if(test -e /mnt/git/branch/$branch/tree){
+		refpath=.git/refs/$branch
+		initial=false
+	}
+	if not if(test -e /mnt/git/object/$branch/tree){
+		refpath=.git/HEAD
+		initial=false
+	}
+	if not if(! test -e /mnt/git/HEAD/tree){
+		refpath=.git/refs/$branch
+		initial=true
+	}
+	if not{
+		echo invalid branch $branch >[1=2]
+		exit badbranch
+	}
+
+}
+
+fn editmsg{
+	echo '' > $msgfile.tmp
+	echo '# Commit message goes here.' >> $msgfile.tmp
+	echo '# Author: '$name' <'$email'>' >> $msgfile.tmp
+	echo '#' $nl'# ' ^ `$nl{git/walk -fAMR} >> $msgfile.tmp
+	sam $msgfile.tmp
+	if(! test -s $msgfile.tmp || ~ `{wc -l <{grep -v '^[ 	]*[\n#]' $msgfile.tmp}} 0){
+		echo 'cancelling commit: empty message' >[1=2]
+		exit 'nocommit'
+	}
+	grep -v '^[ 	]*#' < $msgfile.tmp > $msgfile
+}
+
+fn parents{
+	if(test -f .git/index9/merge-parents)
+		parents=`{cat .git/index9/merge-parents}
+	if not if(~ $initial true)
+		parents=()
+	if not
+		parents=$branch
+}
+
+fn commit{
+	msg=`"{cat $msgfile}
+	if(! ~ $#parents 0)
+		pflags='-p'^$parents
+	hash=`{git/save -n $"name -e $"email  -m $"msg $pflags}
+	st=$status
+	if(~ $hash '')
+		exit nocommit
+}
+
+fn update{
+	mkdir -p `{basename -d $refpath}
+	echo $hash > $refpath
+	for(f in `$nl{git/walk -cfAM}){
+		mkdir -p `{basename -d $f}
+		walk -eq $f > .git/index9/tracked/$f
+	}
+	for(f in `$nl{git/walk -cfR}){
+		rm -f .git/index9/tracked/$f
+		rm -f .git/index9/removed/$f
+	}
+
+}
+
+fn cleanup{
+	rm -f $msgfile
+	rm -f .git/index9/merge-parents
+}
+
+msgfile=/tmp/git-msg.$pid
+if(! cd `{git/conf -r})
+	exit 'not in git repository'
+git/fs
+mkdir -p .git/refs
+if(git/walk -q){
+	echo no changes to commit >[1=2]
+	exit clean
+}
+@{
+	flag e +
+	whoami
+	findbranch
+	parents
+	editmsg
+	commit
+	update
+	cleanup
+}
+st=$status
+if(! ~ $status ''){
+	echo 'could not commit:' $st >[1=2]
+	exit $st
+}
--- /dev/null
+++ b/conf.c
@@ -1,0 +1,102 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+static int
+showconf(char *cfg, char *sect, char *key)
+{
+	char *ln, *p;
+	Biobuf *f;
+	int foundsect, nsect, nkey;
+
+	if((f = Bopen(cfg, OREAD)) == nil)
+		return 0;
+
+	nsect = sect ? strlen(sect) : 0;
+	nkey = strlen(key);
+	foundsect = (sect == nil);
+	while((ln = Brdstr(f, '\n', 1)) != nil){
+		p = strip(ln);
+		if(*p == '[' && sect){
+			foundsect = strncmp(sect, ln, nsect) == 0;
+		}else if(foundsect && strncmp(p, key, nkey) == 0){
+			p = strip(p + nkey);
+			if(*p != '=')
+				continue;
+			p = strip(p + 1);
+			print("%s\n", p);
+			free(ln);
+			return 1;
+		}
+		free(ln);
+	}
+	return 0;
+}
+
+static void
+showroot(void)
+{
+	char path[256], buf[256], *p;
+
+	if((getwd(path, sizeof(path))) == nil)
+		sysfatal("could not get wd: %r");
+	while((p = strrchr(path, '/')) != nil){
+		snprint(buf, sizeof(buf), "%s/.git", path);
+		if(access(buf, AEXIST) == 0){
+			print("%s\n", path);
+			return;
+		}
+		*p = '\0';
+	}
+	sysfatal("not a git repository");
+}
+
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-f file] [-r] keys..\n", argv0);
+	fprint(2, "\t-f:	use file 'file' (default: .git/config)\n");
+	fprint(2, "\t r:	print repository root\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *file[32], *p, *s;
+	int i, j, nfile, findroot;
+
+	nfile = 0;
+	findroot = 0;
+	ARGBEGIN{
+	case 'f':	file[nfile++]=EARGF(usage());	break;
+	case 'r':	findroot++;			break;
+	default:	usage();			break;
+	}ARGEND;
+
+	if(findroot)
+		showroot();
+	if(nfile == 0){
+		file[nfile++] = ".git/config";
+		if((p = getenv("home")) != nil)
+			file[nfile++] = smprint("%s/lib/git/config", p);
+	}
+
+	for(i = 0; i < argc; i++){
+		if((p = strchr(argv[i], '.')) == nil){
+			s = nil;
+			p = argv[i];
+		}else{
+			*p = 0;
+			p++;
+			s = smprint("[%s]", argv[i]);
+		}
+		for(j = 0; j < nfile; j++)
+			if(showconf(file[j], s, p))
+				break;
+	}
+	exits(nil);
+}
--- /dev/null
+++ b/diff
@@ -1,0 +1,40 @@
+#!/bin/rc
+
+rfork e
+
+fn usage{
+	echo git/diff '[-b branch] [file ...]' >[2=1]
+	exit usage
+}
+
+if(! cd `{git/conf -r}){
+	echo 'not a git repository' >[1=2]
+	exit notgit
+}
+git/fs
+
+while(~ $1 -*){
+	switch($1){
+	case -b; branch=$2; shift
+	case *; usage
+	}
+	shift
+}
+
+if(~ $#branch 0)
+	branch=`{git/branch}
+dirty=`{git/walk | awk '/^[MAR]/ {print $2}'}
+if(! ~ $#* 0){
+	echo $dirty | sed 's/ /\n/g' | sort >/tmp/git.$pid.diff.dirty
+	echo $* | sed 's/ /\n/g' | sort >/tmp/git.$pid.diff.args
+	dirty=`{join /tmp/git.$pid.diff.dirty /tmp/git.$pid.diff.args}
+}
+for(f in $dirty){
+	orig=/mnt/git/branch/$branch/tree/$f
+	if(! test -f $orig)
+		orig=/dev/null
+	if(! test -f $f)
+		f=/dev/null
+	ape/diff -up $orig $f
+}
+rm -f /tmp/git.$pid.diff.dirty /tmp/git.$pid.diff.args
--- /dev/null
+++ b/fetch.c
@@ -1,0 +1,285 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+Object *indexed;
+char *fetchbranch;
+char *upstream = "origin";
+char *packtmp = ".git/objects/pack/fetch.tmp";
+
+int
+resolveremote(Hash *h, char *ref)
+{
+	char buf[128], *s;
+	int r, f;
+
+	ref = strip(ref);
+	if((r = hparse(h, ref)) != -1)
+		return r;
+	/* Slightly special handling: translate remote refs to local ones. */
+	if(strcmp(ref, "HEAD") == 0){
+		snprint(buf, sizeof(buf), ".git/HEAD");
+	}else if(strstr(ref, "refs/heads") == ref){
+		ref += strlen("refs/heads");
+		snprint(buf, sizeof(buf), ".git/refs/remotes/%s/%s", upstream, ref);
+	}else if(strstr(ref, "refs/tags") == ref){
+		ref += strlen("refs/tags");
+		snprint(buf, sizeof(buf), ".git/refs/tags/%s/%s", upstream, ref);
+	}else{
+		return -1;
+	}
+
+	s = strip(buf);
+	if((f = open(s, OREAD)) == -1)
+		return -1;
+	if(readn(f, buf, sizeof(buf)) >= 40)
+		r = hparse(h, buf);
+	close(f);
+
+	if(r == -1 && strstr(buf, "ref:") == buf)
+		return resolveremote(h, buf + strlen("ref:"));
+	
+	return r;
+}
+
+int
+rename(char *pack, char *idx, Hash h)
+{
+	char name[128];
+	Dir st;
+
+	nulldir(&st);
+	st.name = name;
+	snprint(name, sizeof(name), "%H.pack", h);
+	if(access(name, AEXIST) == 0)
+		fprint(2, "warning, pack %s already fetched\n", name);
+	else if(dirwstat(pack, &st) == -1)
+		return -1;
+	snprint(name, sizeof(name), "%H.idx", h);
+	if(access(name, AEXIST) == 0)
+		fprint(2, "warning, pack %s already indexed\n", name);
+	else if(dirwstat(idx, &st) == -1)
+		return -1;
+	return 0;
+}
+
+int
+checkhash(int fd, vlong sz, Hash *hcomp)
+{
+	DigestState *st;
+	Hash hexpect;
+	char buf[65536];
+	vlong n, r;
+	int nr;
+	
+	if(sz < 28){
+		werrstr("undersize packfile");
+		return -1;
+	}
+
+	st = nil;
+	n = 0;
+	while(n != sz - 20){
+		nr = sizeof(buf);
+		if(sz - n - 20 < sizeof(buf))
+			nr = sz - n - 20;
+		r = readn(fd, buf, nr);
+		if(r != nr)
+			return -1;
+		st = sha1((uchar*)buf, nr, nil, st);
+		n += r;
+	}
+	sha1(nil, 0, hcomp->h, st);
+	if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h))
+		sysfatal("truncated packfile");
+	if(!hasheq(hcomp, &hexpect)){
+		werrstr("bad hash: %H != %H", *hcomp, hexpect);
+		return -1;
+	}
+	return 0;
+}
+
+int
+mkoutpath(char *path)
+{
+	char s[128];
+	char *p;
+	int fd;
+
+	snprint(s, sizeof(s), "%s", path);
+	for(p=strchr(s+1, '/'); p; p=strchr(p+1, '/')){
+		*p = 0;
+		if(access(s, AEXIST) != 0){
+			fd = create(s, OREAD, DMDIR | 0755);
+			if(fd == -1)
+				return -1;
+			close(fd);
+		}		
+		*p = '/';
+	}
+	return 0;
+}
+
+int
+branchmatch(char *br, char *pat)
+{
+	char name[128];
+
+	if(strstr(pat, "refs/heads") == pat)
+		snprint(name, sizeof(name), "%s", pat);
+	else if(strstr(pat, "heads"))
+		snprint(name, sizeof(name), "refs/%s", pat);
+	else
+		snprint(name, sizeof(name), "refs/heads/%s", pat);
+	return strcmp(br, name) == 0;
+}
+
+int
+fetchpack(int fd, int pfd, char *packtmp)
+{
+	char buf[65536];
+	char idxtmp[256];
+	char *sp[3];
+	Hash h, *have, *want;
+	int nref, refsz;
+	int i, n, req;
+	vlong packsz;
+
+	nref = 0;
+	refsz = 16;
+	have = emalloc(refsz * sizeof(have[0]));
+	want = emalloc(refsz * sizeof(want[0]));
+	while(1){
+		n = readpkt(fd, buf, sizeof(buf));
+		if(n == -1)
+			return -1;
+		if(n == 0)
+			break;
+		if(strncmp(buf, "ERR ", 4) == 0)
+			sysfatal("%s", buf + 4);
+		getfields(buf, sp, nelem(sp), 1, " \t\n\r");
+		if(strstr(sp[1], "^{}"))
+			continue;
+		if(fetchbranch && !branchmatch(sp[1], fetchbranch))
+			continue;
+		if(refsz == nref + 1){
+			refsz *= 2;
+			have = erealloc(have, refsz * sizeof(have[0]));
+			want = erealloc(want, refsz * sizeof(want[0]));
+		}
+		if(hparse(&want[nref], sp[0]) == -1)
+			sysfatal("invalid hash %s", sp[0]);
+		if (resolveremote(&have[nref], sp[1]) == -1)
+			memset(&have[nref], 0, sizeof(have[nref]));
+		print("remote %s %H local %H\n", sp[1], want[nref], have[nref]);
+		nref++;
+	}
+
+	req = 0;
+	for(i = 0; i < nref; i++){
+		if(memcmp(have[i].h, want[i].h, sizeof(have[i].h)) == 0)
+			continue;
+		n = snprint(buf, sizeof(buf), "want %H", want[i]);
+		print("want %H\n", want[i]);
+		if(writepkt(fd, buf, n) == -1)
+			sysfatal("could not send want for %H", want[i]);
+		req = 1; 
+	}
+	flushpkt(fd);
+	for(i = 0; i < nref; i++){
+		if(memcmp(have[i].h, Zhash.h, sizeof(Zhash.h)) == 0)
+			continue;
+		n = snprint(buf, sizeof(buf), "have %H\n", have[i]);
+		if(writepkt(fd, buf, n + 1) == -1)
+			sysfatal("could not send have for %H", have[i]);
+	}
+	if(!req){
+		fprint(2, "up to date\n");
+		flushpkt(fd);
+	}
+	n = snprint(buf, sizeof(buf), "done\n");
+	if(writepkt(fd, buf, n) == -1)
+		sysfatal("lost connection write");
+	if(!req)
+		return 0;
+
+	if((n = readpkt(fd, buf, sizeof(buf))) == -1)
+		sysfatal("lost connection read");
+	buf[n] = 0;
+
+	fprint(2, "fetching...\n");
+	packsz = 0;
+	while(1){
+		n = readn(fd, buf, sizeof buf);
+		if(n == 0)
+			break;
+		if(n == -1 || write(pfd, buf, n) != n)
+			sysfatal("could not fetch packfile: %r");
+		packsz += n;
+	}
+	if(seek(pfd, 0, 0) == -1)
+		sysfatal("packfile seek: %r");
+	if(checkhash(pfd, packsz, &h) == -1)
+		sysfatal("corrupt packfile: %r");
+	close(pfd);
+	n = strlen(packtmp) - strlen(".tmp");
+	memcpy(idxtmp, packtmp, n);
+	memcpy(idxtmp + n, ".idx", strlen(".idx") + 1);
+	if(indexpack(packtmp, idxtmp, h) == -1)
+		sysfatal("could not index fetched pack: %r");
+	if(rename(packtmp, idxtmp, h) == -1)
+		sysfatal("could not rename indexed pack: %r");
+	return 0;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-b br] remote\n", argv0);
+	fprint(2, "\t-b br:	only fetch matching branch 'br'\n");
+	fprint(2, "remote:	fetch from this repository\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char proto[Nproto], host[Nhost], port[Nport];
+	char repo[Nrepo], path[Npath];
+	int fd, pfd;
+
+	ARGBEGIN{
+	case 'b':	fetchbranch=EARGF(usage());	break;
+	case 'u':	upstream=EARGF(usage());	break;
+	case 'd':	chattygit++;			break;
+	default:	usage();			break;
+	}ARGEND;
+
+	gitinit();
+	if(argc != 1)
+		usage();
+	fd = -1;
+
+	if(mkoutpath(packtmp) == -1)
+		sysfatal("could not create %s: %r", packtmp);
+	if((pfd = create(packtmp, ORDWR, 0644)) == -1)
+		sysfatal("could not create %s: %r", packtmp);
+
+	if(parseuri(argv[0], proto, host, port, path, repo) == -1)
+		sysfatal("bad uri %s", argv[0]);
+	if(strcmp(proto, "ssh") == 0 || strcmp(proto, "git+ssh") == 0)
+		fd = dialssh(host, port, path, "upload");
+	else if(strcmp(proto, "git") == 0)
+		fd = dialgit(host, port, path, "upload");
+	else if(strcmp(proto, "http") == 0 || strcmp(proto, "git+http") == 0)
+		sysfatal("http clone not implemented");
+	else
+		sysfatal("unknown protocol %s", proto);
+	
+	if(fd == -1)
+		sysfatal("could not dial %s:%s: %r", proto, host);
+	if(fetchpack(fd, pfd, packtmp) == -1)
+		sysfatal("fetch failed: %r");
+	exits(nil);
+}
--- /dev/null
+++ b/fs.c
@@ -1,0 +1,756 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "git.h"
+
+typedef struct Ols Ols;
+
+char *Eperm = "permission denied";
+char *Eexist = "does not exist";
+char *E2long = "path too long";
+char *Enodir = "not a directory";
+char *Erepo = "unable to read repo";
+char *Egreg = "wat";
+
+enum {
+	Qroot,
+	Qhead,
+	Qbranch,
+	Qcommit,
+	Qcommitmsg,
+	Qcommitparent,
+	Qcommittree,
+	Qcommitdata,
+	Qcommithash,
+	Qcommitauthor,
+	Qobject,
+	Qctl,
+	Qmax,
+	Internal=1<<7,
+};
+
+typedef struct Gitaux Gitaux;
+struct Gitaux {
+	int	 npath;
+	Qid	 path[Npath];
+	Object  *opath[Npath];
+	char 	*refpath;
+	int	 qdir;
+	vlong	 mtime;
+	Object	*obj;
+
+	/* For listing object dir */
+	Ols	*ols;
+	Object	*olslast;
+};
+
+char *qroot[] = {
+	"HEAD",
+	"branch",
+	"object",
+	"ctl",
+};
+
+char *username;
+char *mtpt = "/mnt/git";
+char **branches = nil;
+
+static vlong
+findbranch(Gitaux *aux, char *path)
+{
+	int i;
+
+	for(i = 0; branches[i]; i++)
+		if(strcmp(path, branches[i]) == 0)
+			goto found;
+	branches = realloc(branches, sizeof(char *)*(i + 2));
+	branches[i] = estrdup(path);
+	branches[i + 1] = nil;
+
+found:
+	if(aux)
+		aux->refpath = estrdup(branches[i]);
+	return QPATH(i, Qbranch|Internal);
+}
+
+static void
+obj2dir(Dir *d, Object *o, long qdir, vlong mtime)
+{
+	char name[64];
+
+	snprint(name, sizeof(name), "%H", o->hash);
+	d->name = estrdup9p(name);
+	d->qid.type = QTDIR;
+	d->qid.path = QPATH(o->id, qdir);
+	d->atime = mtime;
+	d->mtime = mtime;
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	d->mode = 0755 | DMDIR;
+	if(o->type == GBlob || o->type == GTag){
+		d->qid.type = 0;
+		d->mode = 0644;
+		d->length = o->size;
+	}
+
+}
+
+static int
+rootgen(int i, Dir *d, void *p)
+{
+	Gitaux *aux;
+
+	aux = p;
+	if (i >= nelem(qroot))
+		return -1;
+	d->mode = 0555 | DMDIR;
+	d->name = estrdup9p(qroot[i]);
+	d->qid.vers = 0;
+	d->qid.type = strcmp(qroot[i], "ctl") == 0 ? 0 : QTDIR;
+	d->qid.path = QPATH(i, Qroot);
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	d->mtime = aux->mtime;
+	return 0;
+}
+
+static int
+branchgen(int i, Dir *d, void *p)
+{
+	Gitaux *aux;
+	Dir *refs;
+	int n;
+
+	aux = p;
+	refs = nil;
+	d->qid.vers = 0;
+	d->qid.type = QTDIR;
+	d->qid.path = findbranch(nil, aux->refpath);
+	d->mode = 0555 | DMDIR;
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	d->mtime = aux->mtime;
+	d->atime = aux->mtime;
+	if((n = slurpdir(aux->refpath, &refs)) < 0)
+		return -1;
+	if(i < n){
+		d->name = estrdup9p(refs[i].name);
+		free(refs);
+		return 0;
+	}else{
+		free(refs);
+		return -1;
+	}
+}
+
+/* FIXME: walk to the appropriate submodule.. */
+static Object*
+modrefobj(Dirent *e)
+{
+	Object *m;
+
+	m = emalloc(sizeof(Object));
+	m->hash = e->h;
+	m->type = GTree;
+	m->tree = emalloc(sizeof(Tree));
+	m->tree->ent = nil;
+	m->tree->nent = 0;
+	m->flag |= Cloaded|Cparsed;
+	m->off = -1;
+	ref(m);
+	cache(m);
+	return m;
+}
+
+static int
+gtreegen(int i, Dir *d, void *p)
+{
+	Gitaux *aux;
+	Object *o, *e;
+
+	aux = p;
+	e = aux->obj;
+	if(i >= e->tree->nent)
+		return -1;
+	if((o = readobject(e->tree->ent[i].h)) == nil)
+		if(e->tree->ent[i].modref)
+			o = modrefobj(&e->tree->ent[i]);
+		else
+			die("could not read object %H: %r", e->tree->ent[i].h, e->hash);
+	d->qid.vers = 0;
+	d->qid.type = o->type == GTree ? QTDIR : 0;
+	d->qid.path = QPATH(o->id, aux->qdir);
+	d->mode = e->tree->ent[i].mode;
+	d->atime = aux->mtime;
+	d->mtime = aux->mtime;
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	d->name = estrdup9p(e->tree->ent[i].name);
+	d->length = o->size;
+	return 0;
+}
+
+static int
+gcommitgen(int i, Dir *d, void *p)
+{
+	Object *o;
+
+	o = ((Gitaux*)p)->obj;
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	d->mode = 0444;
+	d->atime = o->commit->ctime;
+	d->mtime = o->commit->ctime;
+	d->qid.type = 0;
+	d->qid.vers = 0;
+
+	switch(i){
+	case 0:
+		d->mode = 0555 | DMDIR;
+		d->name = estrdup9p("tree");
+		d->qid.type = QTDIR;
+		d->qid.path = QPATH(o->id, Qcommittree);
+		break;
+	case 1:
+		d->name = estrdup9p("parent");
+		d->qid.path = QPATH(o->id, Qcommitparent);
+		break;
+	case 2:
+		d->name = estrdup9p("msg");
+		d->qid.path = QPATH(o->id, Qcommitmsg);
+		break;
+	case 3:
+		d->name = estrdup9p("hash");
+		d->qid.path = QPATH(o->id, Qcommithash);
+		break;
+	case 4:
+		d->name = estrdup9p("author");
+		d->qid.path = QPATH(o->id, Qcommitauthor);
+		break;
+	default:
+		return -1;
+	}
+	return 0;
+}
+
+
+static int
+objgen(int i, Dir *d, void *p)
+{
+	Gitaux *aux;
+	Object *o;
+	Ols *ols;
+	Hash h;
+
+	aux = p;
+	if(!aux->ols)
+		aux->ols = mkols();
+	ols = aux->ols;
+	o = nil;
+	/* We tried to sent it, but it didn't fit */
+	if(aux->olslast && ols->idx == i + 1){
+		obj2dir(d, aux->olslast, Qobject, aux->mtime);
+		return 0;
+	}
+	while(ols->idx <= i){
+		if(olsnext(ols, &h) == -1)
+			return -1;
+		if((o = readobject(h)) == nil)
+			return -1;
+	}
+	if(o != nil){
+		obj2dir(d, o, Qobject, aux->mtime);
+		unref(aux->olslast);
+		aux->olslast = ref(o);
+		return 0;
+	}
+	return -1;
+}
+
+static void
+objread(Req *r, Gitaux *aux)
+{
+	Object *o;
+
+	o = aux->obj;
+	switch(o->type){
+	case GBlob:
+		readbuf(r, o->data, o->size);
+		break;
+	case GTag:
+		readbuf(r, o->data, o->size);
+		break;
+	case GTree:
+		dirread9p(r, gtreegen, aux);
+		break;
+	case GCommit:
+		dirread9p(r, gcommitgen, aux);
+		break;
+	default:
+		die("invalid object type %d", o->type);
+	}
+}
+
+static void
+readcommitparent(Req *r, Object *o)
+{
+	char *buf, *p;
+	int i, n;
+
+	n = o->commit->nparent * (40 + 2);
+	buf = emalloc(n);
+	p = buf;
+	for (i = 0; i < o->commit->nparent; i++)
+		p += sprint(p, "%H\n", o->commit->parent[i]);
+	readbuf(r, buf, n);
+	free(buf);
+}
+
+
+static void
+gitattach(Req *r)
+{
+	Gitaux *aux;
+	Dir *d;
+
+	if((d = dirstat(".git")) == nil)
+		sysfatal("git/fs: %r");
+	aux = emalloc(sizeof(Gitaux));
+	aux->path[0] = (Qid){Qroot, 0, QTDIR};
+	aux->opath[0] = nil;
+	aux->npath = 1;
+	aux->mtime = d->mtime;
+	r->ofcall.qid = (Qid){Qroot, 0, QTDIR};
+	r->fid->qid = r->ofcall.qid;
+	r->fid->aux = aux;
+	respond(r, nil);
+}
+
+static char *
+objwalk1(Qid *q, Gitaux *aux, char *name, vlong qdir)
+{
+	Object *o, *w;
+	char *e;
+	int i;
+
+	w = nil;
+	e = nil;
+	o = aux->obj;
+	if(!o)
+		return Eexist;
+	if(o->type == GTree){
+		q->type = 0;
+		for(i = 0; i < o->tree->nent; i++){
+			if(strcmp(o->tree->ent[i].name, name) != 0)
+				continue;
+			w = readobject(o->tree->ent[i].h);
+			if(!w && o->tree->ent[i].modref)
+				w = modrefobj(&o->tree->ent[i]);
+			if(!w)
+				die("could not read object for %s", name);
+			q->type = (w->type == GTree) ? QTDIR : 0;
+			q->path = QPATH(w->id, qdir);
+			aux->obj = w;
+		}
+		if(!w)
+			e = Eexist;
+	}else if(o->type == GCommit){
+		q->type = 0;
+		aux->mtime = o->commit->mtime;
+		assert(qdir == Qcommit || qdir == Qobject || qdir == Qcommittree || qdir == Qhead);
+		if(strcmp(name, "msg") == 0)
+			q->path = QPATH(o->id, Qcommitmsg);
+		else if(strcmp(name, "parent") == 0 && o->commit->nparent != 0)
+			q->path = QPATH(o->id, Qcommitparent);
+		else if(strcmp(name, "hash") == 0)
+			q->path = QPATH(o->id, Qcommithash);
+		else if(strcmp(name, "author") == 0)
+			q->path = QPATH(o->id, Qcommitauthor);
+		else if(strcmp(name, "tree") == 0){
+			q->type = QTDIR;
+			q->path = QPATH(o->id, Qcommittree);
+			aux->obj = readobject(o->commit->tree);
+		}
+		else
+			e = Eexist;
+	}else if(o->type == GTag){
+		e = "tag walk unimplemented";
+	}
+	return e;
+}
+
+static Object *
+readref(char *pathstr)
+{
+	char buf[128], path[128], *p, *e;
+	Hash h;
+	int n, f;
+
+	snprint(path, sizeof(path), "%s", pathstr);
+	while(1){
+		if((f = open(path, OREAD)) == -1)
+			return nil;
+		if((n = readn(f, buf, sizeof(buf) - 1)) == -1)
+			return nil;
+		close(f);
+		buf[n] = 0;
+		if(strncmp(buf, "ref:", 4) !=  0)
+			break;
+
+		p = buf + 4;
+		while(isspace(*p))
+			p++;
+		if((e = strchr(p, '\n')) != nil)
+			*e = 0;
+		snprint(path, sizeof(path), ".git/%s", p);
+	}
+
+	if(hparse(&h, buf) == -1){
+		print("failed to parse hash %s\n", buf);
+		return nil;
+	}
+
+	return readobject(h);
+}
+
+static char*
+gitwalk1(Fid *fid, char *name, Qid *q)
+{
+	char path[128];
+	Gitaux *aux;
+	Object *o;
+	char *e;
+	Dir *d;
+	Hash h;
+
+	e = nil;
+	aux = fid->aux;
+	q->vers = 0;
+
+	if(strcmp(name, "..") == 0){
+		if(aux->npath > 1)
+			aux->npath--;
+		*q = aux->path[aux->npath - 1];
+		o = ref(aux->opath[aux->npath - 1]);
+		if(aux->obj)
+			unref(aux->obj);
+		aux->obj = o;
+		fid->qid = *q;
+		return nil;
+	}
+	
+
+	switch(QDIR(&fid->qid)){
+	case Qroot:
+		if(strcmp(name, "HEAD") == 0){
+			*q = (Qid){Qhead, 0, QTDIR};
+			aux->obj = readref(".git/HEAD");
+		}else if(strcmp(name, "object") == 0){
+			*q = (Qid){Qobject, 0, QTDIR};
+		}else if(strcmp(name, "branch") == 0){
+			*q = (Qid){Qbranch, 0, QTDIR};
+			aux->refpath = estrdup(".git/refs/");
+		}else if(strcmp(name, "ctl") == 0){
+			*q = (Qid){Qctl, 0, 0};
+		}else{
+			e = Eexist;
+		}
+		break;
+	case Qbranch:
+		if(strcmp(aux->refpath, ".git/refs/heads") == 0 && strcmp(name, "HEAD") == 0)
+			snprint(path, sizeof(path), ".git/HEAD");
+		else
+			snprint(path, sizeof(path), "%s/%s", aux->refpath, name);
+		q->type = QTDIR;
+		d = dirstat(path);
+		if(d && d->qid.type == QTDIR)
+			q->path = QPATH(findbranch(aux, path), Qbranch);
+		else if(d && (aux->obj = readref(path)) != nil)
+			q->path = QPATH(aux->obj->id, Qcommit);
+		else
+			e = Eexist;
+		free(d);
+		break;
+	case Qobject:
+		if(aux->obj){
+			e = objwalk1(q, aux, name, Qobject);
+		}else{
+			if(hparse(&h, name) == -1)
+				return "invalid object name";
+			if((aux->obj = readobject(h)) == nil)
+				return "could not read object";
+			q->path = QPATH(aux->obj->id, Qobject);
+			q->type = (aux->obj->type == GBlob) ? 0 : QTDIR;
+			q->vers = 0;
+		}
+		break;
+	case Qhead:
+		e = objwalk1(q, aux, name, Qhead);
+		break;
+	case Qcommit:
+		e = objwalk1(q, aux, name, Qcommit);
+		break;
+	case Qcommittree:
+		e = objwalk1(q, aux, name, Qcommittree);
+		break;
+	case Qcommitparent:
+	case Qcommitmsg:
+	case Qcommitdata:
+	case Qcommithash:
+	case Qcommitauthor:
+	case Qctl:
+		return Enodir;
+	default:
+		die("walk: bad qid %Q", *q);
+	}
+	if(aux->npath >= Npath)
+		e = E2long;
+	if(!e && QDIR(q) >= Qmax){
+		print("npath: %d\n", aux->npath);
+		print("walking to %llx (name: %s)\n", q->path, name);
+		print("walking from %llx\n", fid->qid.path);
+		print("QDIR=%d\n", QDIR(&fid->qid));
+		if(aux->obj)
+			print("obj=%O\n", aux->obj);
+		abort();
+	}
+
+	aux->path[aux->npath] = *q;
+	if(aux->obj)
+		aux->opath[aux->npath] = ref(aux->obj);
+	aux->npath++;
+	fid->qid = *q;
+	return e;
+}
+
+static char*
+gitclone(Fid *o, Fid *n)
+{
+	Gitaux *aux, *oaux;
+	int i;
+
+	oaux = o->aux;
+	aux = emalloc(sizeof(Gitaux));
+	aux->npath = oaux->npath;
+	for(i = 0; i < aux->npath; i++){
+		aux->path[i] = oaux->path[i];
+		aux->opath[i] = oaux->opath[i];
+		if(aux->opath[i])
+			ref(aux->opath[i]);
+	}
+	if(oaux->refpath)
+		aux->refpath = strdup(oaux->refpath);
+	if(oaux->obj)
+		aux->obj = ref(oaux->obj);
+	aux->qdir = oaux->qdir;
+	aux->mtime = oaux->mtime;
+	n->aux = aux;
+	return nil;
+}
+
+static void
+gitdestroyfid(Fid *f)
+{
+	Gitaux *aux;
+	int i;
+
+	if((aux = f->aux) == nil)
+		return;
+	for(i = 0; i < aux->npath; i++)
+		unref(aux->opath[i]);
+	free(aux->refpath);
+	olsfree(aux->ols);
+	unref(aux->obj);
+	free(aux);
+}
+
+static char *
+readctl(Req *r)
+{
+	char data[512], buf[512], *p;
+	int fd, n;
+	if((fd = open(".git/HEAD", OREAD)) == -1)
+		return Erepo;
+	/* empty HEAD is invalid */
+	if((n = readn(fd, buf, sizeof(buf) - 1)) <= 0)
+		return Erepo;
+	close(fd);
+	p = buf;
+	buf[n] = 0;
+	if(strstr(p, "ref: ") == buf)
+		p += strlen("ref: ");
+	if(strstr(p, "refs/") == p)
+		p += strlen("refs/");
+	snprint(data, sizeof(data), "branch %s", p);
+	readstr(r, data);
+	return nil;
+}
+
+static void
+gitread(Req *r)
+{
+	char buf[64], *e;
+	Gitaux *aux;
+	Object *o;
+	Qid *q;
+
+	q = &r->fid->qid;
+	o = nil;
+	e = nil;
+	if(aux = r->fid->aux)
+		o = aux->obj;
+
+	switch(QDIR(q)){
+	case Qroot:
+		dirread9p(r, rootgen, aux);
+		break;
+	case Qbranch:
+		if(o)
+			objread(r, aux);
+		else
+			dirread9p(r, branchgen, aux);
+		break;
+	case Qobject:
+		if(o)
+			objread(r, aux);
+		else
+			dirread9p(r, objgen, aux);
+		break;
+	case Qcommitmsg:
+		readbuf(r, o->commit->msg, o->commit->nmsg);
+		break;
+	case Qcommitparent:
+		readcommitparent(r, o);
+		break;
+	case Qcommithash:
+		snprint(buf, sizeof(buf), "%H\n", o->hash);
+		readstr(r, buf);
+		break;
+	case Qcommitauthor:
+		readstr(r, o->commit->author);
+		break;
+	case Qctl:
+		e = readctl(r);
+		break;
+	case Qhead:
+		/* Empty repositories have no HEAD */
+		if(aux->obj == nil)
+			r->ofcall.count = 0;
+		else
+			objread(r, aux);
+		break;
+	case Qcommit:
+	case Qcommittree:
+	case Qcommitdata:
+		objread(r, aux);
+		break;
+	default:
+		die("read: bad qid %Q", *q);
+	}
+	respond(r, e);
+}
+
+static void
+gitstat(Req *r)
+{
+	Gitaux *aux;
+	Qid *q;
+
+	q = &r->fid->qid;
+	aux = r->fid->aux;
+	r->d.uid = estrdup9p(username);
+	r->d.gid = estrdup9p(username);
+	r->d.muid = estrdup9p(username);
+	r->d.mtime = aux->mtime;
+	r->d.atime = r->d.mtime;
+	r->d.qid = r->fid->qid;
+	r->d.mode = 0755 | DMDIR;
+	if(aux->obj){
+		obj2dir(&r->d, aux->obj, QDIR(q), aux->mtime);
+	} else {
+		switch(QDIR(q)){
+		case Qroot:
+			r->d.name = estrdup9p("/");
+			break;
+		case Qhead:
+			r->d.name = estrdup9p("HEAD");
+			break;
+		case Qbranch:
+			r->d.name = estrdup9p("branch");
+			break;
+		case Qobject:
+			r->d.name = estrdup9p("object");
+			break;
+		case Qctl:
+			r->d.name = estrdup9p("ctl");
+			r->d.mode = 0666;
+			break;
+		case Qcommit:
+			r->d.name = smprint("%H", aux->obj->hash);
+			break;
+		case Qcommitmsg:
+			r->d.name = estrdup9p("msg");
+			r->d.mode = 0644;
+			break;
+		case Qcommittree:
+			r->d.name = estrdup9p("tree");
+			break;
+		case Qcommitparent:
+			r->d.name = estrdup9p("info");
+			r->d.mode = 0644;
+			break;
+		case Qcommithash:
+			r->d.name = estrdup9p("hash");
+			r->d.mode = 0644;
+			break;
+		default:
+			die("stat: bad qid %Q", *q);
+		}
+	}
+
+	respond(r, nil);
+}
+
+Srv gitsrv = {
+	.attach=gitattach,
+	.walk1=gitwalk1,
+	.clone=gitclone,
+	.read=gitread,
+	.stat=gitstat,
+	.destroyfid=gitdestroyfid,
+};
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-d]\n", argv0);
+	fprint(2, "\t-d:	debug\n");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	gitinit();
+	ARGBEGIN{
+	case 'd':	chatty9p++;	break;
+	default:	usage();	break;
+	}ARGEND;
+	if(argc != 0)
+		usage();
+
+	username = getuser();
+	branches = emalloc(sizeof(char*));
+	branches[0] = nil;
+	postmountsrv(&gitsrv, nil, "/mnt/git", MCREATE);
+	exits(nil);
+}
--- /dev/null
+++ b/git.1
@@ -1,0 +1,397 @@
+.TH GIT 1
+.SH NAME
+git, git/conf, git/fs, git/query, git/walk, git/clone, git/branch,
+git/commit, git/diff, git/init, git/log, git/merge, git/push, git/pull
+\- Manage git repositories.
+
+.SH SYNOPSIS
+.PP
+.B git/add
+[
+.B -r
+]
+.I path...
+.PP
+.B git/branch
+[
+.B -b
+.I base
+]
+.I newbranch
+.PP
+.B git/clone
+[
+.I remote
+[
+.I local
+]
+]
+.PP
+.B git/commit
+.PP
+.B git/conf
+[
+.B -r
+]
+[
+.B -f
+.I file
+]
+.I keys...
+.PP
+.B git/diff
+[
+.B -b
+.I branch
+]
+[
+.I file...
+]
+.PP
+.B git/fs
+[
+.B -d
+]
+.PP
+.B git/init
+[
+.B -b
+]
+[
+.I dir
+]
+[
+.B -u
+.I upstream
+]
+.PP
+.B git/log
+.PP
+.B git/merge
+.I theirs
+.PP
+.B git/pull
+[
+.B -f
+]
+[
+.B -u
+.I upstream
+]
+.PP
+.B git/push
+[
+.B -a
+]
+[
+.B -u
+.I upstream
+]
+[
+.B -b
+.I branch
+]
+[
+.B -r
+.I branch
+]
+.PP
+.B git/query
+[
+.B -p
+]
+.PP
+.B git/walk
+[
+.B -qc
+]
+[
+.B -b
+.I branch
+]
+[
+.B -f
+.I filters
+]
+
+.SH DESCRIPTION
+.PP
+Git is a distributed version control system.
+This means that each repository contains a full copy of the history.
+This history is then synced between computers as needed.
+
+.PP
+These programs provide tools to manage and interoperate with
+repositories hosted in git.
+
+.SH CONCEPTS
+
+Git stores snapshots of the working directory.
+Files can either be in a tracked or untracked state.
+Each commit takes the current version of all tracked files and
+adds them to a new commit.
+
+This history is stored in the
+.I .git
+directory.
+This suite of
+.I git
+tools provides a file interface to the
+.I .git
+directory mounted on
+.I /mnt/git.
+Modifications to the repository are done directly to the
+.I .git
+directory, and are reflected in the file system interface.
+This allows for easy scripting, without excessive complexity
+in the file API.
+
+.SH COMMANDS
+
+.PP
+.B Git/init
+is used to create a new git repository, with no code or commits.
+The repository is created in the current directory by default.
+Passing a directory name will cause the repository to be created
+there instead.
+Passing the
+.B -b
+option will cause the repository to be initialized as a bare repository.
+Passing the
+.B -u
+.I upstream
+option will cause the upstream to be configured to
+.I upstream.
+
+.PP
+.B Git/clone
+will take an existing repository, served over either the
+.I git://
+or
+.I ssh://
+protocols.
+The first argument is the repository to clone.
+The second argument, optionally, specifies the location to clone into.
+If not specified, the repository will be cloned into the last path component
+of the clone source, with the
+.I .git
+stripped off if present.
+
+.B Git/push
+is used to push the current changes to a remote repository.
+When no arguments are provided, the remote repository is taken from
+the origin configured in
+.I .git/config,
+and only the changes on the current branch are pushed.
+When passed the
+.I -a
+option, all branches are pushed.
+When passed the
+.I -u upstream
+option, the changed are pushed to
+.I upstream
+instead of the configured origin.
+When given the
+.I -r
+option, the branch is deleted from origin, instead of updated.
+
+.B Git/pull
+behaves in a similar manner to git/push, however it gets changes from
+the upstream repository.
+After fetching, it checks out the changes into the working directory.
+When passed the
+.I -f
+option, the update of the working copy is suppressed.
+When passed the
+.I -u upstream
+option, the changes are pulled from
+.I upstream
+instead of the configured origin.
+
+.B Git/fs
+provides a read-only file system mounted on /mnt/git.
+It provides a  view into the repository contents to allow convenient inspection of repository structure.
+Surrounding scripts and binaries manipulate the repository contents directly.
+These changes will be immediately mirrored in the file system.
+
+.PP
+Git/fs serves the following directories:
+
+.TP
+/mnt/git/object
+The objects in the repo.
+.TP
+/mnt/git/branch
+The branches and tags in the repo.
+.TP
+/mnt/git/ctl
+A file showing the status of the repo.
+Currently, it only shows the current branch.
+.TP
+/mnt/git/HEAD
+An alias for the currently checked out commit directory.
+
+.PP
+.B Git/add
+adds a file to the list of tracked files. When passed the
+.I -r
+flag, the file is removed from the list of tracked files.
+The copy of the file in the repository is left untouched.
+
+.PP
+.B Git/commit
+creates a new commit reflecting the state of the current repository.
+It takes no arguments: all files must be added or removed using git/add.
+If a file does not exist in the working directory, it is treated as though it was removed.
+
+.PP
+.B Git/branch
+is used to switch between branches.
+When passed the
+.I -c
+option, the branch will be created if it does not yet exist.
+When passed the
+.I -o original
+option, the branch created is based off of
+.I original
+instead of
+.I HEAD.
+When passed the
+.I -s
+option, the branch is created but the files are not checked out.
+
+.PP
+.B Git/log
+shows a history of the current branch.
+
+.PP
+.B Git/diff
+shows the differences between the currently checked out code and
+the
+.I HEAD
+commit.
+When passed the
+.I -b base
+option, the diff is computed against
+.I base
+instead of
+.I HEAD.
+
+.PP
+.B Git/merge
+takes two branches and merges them filewise using
+.I ape/diff3.
+The next commit made will be a merge commmit.
+
+.PP
+.B Git/conf
+is a tool for querying the git configuration.
+The configuration key is provided as a dotted string. Spaces
+are accepted. For example, to find the URL of the origin
+repository, one might pass
+.I 'remote "origin".url".
+When given the
+.I -r
+option, the root of the current repository is printed.
+
+.B Git/query
+takes an expression describing a commit, or set of commits,
+and resolves it to a list of commits. With the
+.I -p
+option, instead of printing the commit hashes, the full
+path to their
+.B git/fs
+path is printed.
+
+.PP
+.B Git/walk
+provides a tool for walking the list of tracked objects and printing their status.
+With no arguments, it prints a list of paths prefixed with the status character.
+When given the
+.I -c
+character, only the paths are printed.
+When given the
+.I -q
+option, all output is suppressed, and only the status is printed.
+When given the
+.I -f
+option, the output is filtered by status code, and only matching items are printed.
+
+.PP
+The status characters are as follows:
+.TP
+T
+Tracked, not modified since last commit.
+.TP
+M
+Modified since last commit.
+.TP
+R
+Removed from either working directory tracking list.
+.TP
+A
+Added, does not yet exist in a commit.
+
+.SH REF SYNTAX
+
+.PP
+Refs are specified with a simple query syntax.
+A bare hash always evaluates to itself.
+Ref names are resolved to their hashes.
+The
+.B a ^
+suffix operator finds the parent of a commit.
+The
+.B a b @
+suffix operator finds the common ancestor of the previous two commits.
+The
+.B a .. b
+or
+.B a : b
+operator finds all commits between
+.B a
+and
+.B b.
+Between is defined as the set of all commits which are ancestors of
+.B b
+and descendants of
+.B a.
+
+.SH EXAMPLES
+
+.PP
+In order to create a new repository, run
+.B git/init:
+
+.EX
+git/init myrepo
+.EE
+
+To clone an existing repository from a git server, run:
+.EX
+git/clone git://github.com/Harvey-OS/harvey
+cd harvey
+# edit files
+git/commit
+git/push
+.EE
+
+.SH SOURCE
+.B /sys/src/cmd/git
+
+.SH SEE ALSO
+.IR hg(1)
+.IR replica(1)
+.IR patch(1)
+.IR diff3
+
+.SH BUGS
+.PP
+Repositories with submodules are effectively read-only.
+
+.PP
+There are a number of missing commands, features, and tools. Notable
+missing features include
+.I http
+clones, history editing, and formatted patch management.
+
--- /dev/null
+++ b/git.h
@@ -1,0 +1,238 @@
+#include <bio.h>
+#include <mp.h>
+#include <libsec.h>
+#include <flate.h>
+#include <regexp.h>
+
+typedef struct Hash	Hash;
+typedef struct Cinfo	Cinfo;
+typedef struct Tinfo	Tinfo;
+typedef struct Object	Object;
+typedef struct Objset	Objset;
+typedef struct Pack	Pack;
+typedef struct Buf	Buf;
+typedef struct Dirent	Dirent;
+typedef struct Idxent	Idxent;
+typedef struct Ols	Ols;
+
+enum {
+	/* 5k objects should be enough */
+	Cachemax=5*1024,
+	Pathmax=512,
+	Hashsz=20,
+
+	Nproto	= 16,
+	Nport	= 16,
+	Nhost	= 256,
+	Npath	= 128,
+	Nrepo	= 64,
+	Nbranch	= 32,
+};
+
+typedef enum Type {
+	GNone	= 0,
+	GCommit	= 1,
+	GTree	= 2,
+	GBlob	= 3,
+	GTag	= 4,
+	GOdelta	= 6,
+	GRdelta	= 7,
+} Type;
+
+enum {
+	Cloaded	= 1 << 0,
+	Cidx	= 1 << 1,
+	Ccache	= 1 << 2,
+	Cexist	= 1 << 3,
+	Cparsed	= 1 << 5,
+};
+
+struct Ols {
+	int idx;
+
+	int fd;
+	int state;
+	int stage;
+
+	Dir *top;
+	int ntop;
+	int topidx;
+	Dir *loose;
+	int nloose;
+	int looseidx;
+	Dir *pack;
+	int npack;
+	int packidx;
+	int nent;
+	int entidx;
+};
+
+struct Hash {
+	uchar h[20];
+};
+
+struct Dirent {
+	char *name;
+	int modref;
+	int mode;
+	Hash h;
+};
+
+struct Object {
+	/* Git data */
+	Hash	hash;
+	Type	type;
+
+	/* Cache */
+	int	id;
+	int	flag;
+	int	refs;
+	Object	*next;
+	Object	*prev;
+
+	/* For indexing */
+	vlong	off;
+
+	/* Everything below here gets cleared */
+	char	*all;
+	char	*data;
+	/* size excludes header */
+	vlong	size;
+
+	union {
+		Cinfo *commit;
+		Tinfo *tree;
+	};
+};
+
+struct Tinfo {
+	/* Tree */
+	Dirent	*ent;
+	int	nent;
+};
+
+struct Cinfo {
+	/* Commit */
+	Hash	*parent;
+	int	nparent;
+	Hash	tree;
+	char	*author;
+	char	*committer;
+	char	*msg;
+	int	nmsg;
+	vlong	ctime;
+	vlong	mtime;
+};
+
+struct Objset {
+	Object	**obj;
+	int	nobj;
+	int	sz;
+};
+
+#define GETBE16(b)\
+		((((b)[0] & 0xFFul) <<  8) | \
+		 (((b)[1] & 0xFFul) <<  0))
+
+#define GETBE32(b)\
+		((((b)[0] & 0xFFul) << 24) | \
+		 (((b)[1] & 0xFFul) << 16) | \
+		 (((b)[2] & 0xFFul) <<  8) | \
+		 (((b)[3] & 0xFFul) <<  0))
+#define GETBE64(b)\
+		((((b)[0] & 0xFFull) << 56) | \
+		 (((b)[1] & 0xFFull) << 48) | \
+		 (((b)[2] & 0xFFull) << 40) | \
+		 (((b)[3] & 0xFFull) << 32) | \
+		 (((b)[4] & 0xFFull) << 24) | \
+		 (((b)[5] & 0xFFull) << 16) | \
+		 (((b)[6] & 0xFFull) <<  8) | \
+		 (((b)[7] & 0xFFull) <<  0))
+
+#define PUTBE16(b, n)\
+	do{ \
+		(b)[0] = (n) >> 8; \
+		(b)[1] = (n) >> 0; \
+	} while(0)
+
+#define PUTBE32(b, n)\
+	do{ \
+		(b)[0] = (n) >> 24; \
+		(b)[1] = (n) >> 16; \
+		(b)[2] = (n) >> 8; \
+		(b)[3] = (n) >> 0; \
+	} while(0)
+
+#define PUTBE64(b, n)\
+	do{ \
+		(b)[0] = (n) >> 56; \
+		(b)[1] = (n) >> 48; \
+		(b)[2] = (n) >> 40; \
+		(b)[3] = (n) >> 32; \
+		(b)[4] = (n) >> 24; \
+		(b)[5] = (n) >> 16; \
+		(b)[6] = (n) >> 8; \
+		(b)[7] = (n) >> 0; \
+	} while(0)
+
+#define QDIR(qid)	((int)(qid)->path & (0xff))
+#define QPATH(id, dt)	(((uvlong)(id) << 8) | ((dt) & 0x7f))
+#define isblank(c) \
+	(((c) != '\n') && isspace(c))
+
+extern Reprog *authorpat;
+extern Objset objcache;
+extern Hash Zhash;
+extern int chattygit;
+
+#pragma varargck type "H" Hash
+#pragma varargck type "T" Type
+#pragma varargck type "O" Object*
+#pragma varargck type "Q" Qid
+int Hfmt(Fmt*);
+int Tfmt(Fmt*);
+int Ofmt(Fmt*);
+int Qfmt(Fmt*);
+
+void gitinit(void);
+
+/* object io */
+int	resolverefs(Hash **, char *);
+int	resolveref(Hash *, char *);
+Object	*readobject(Hash);
+void	parseobject(Object *);
+int	indexpack(char *, char *, Hash);
+int	hasheq(Hash *, Hash *);
+Object	*ref(Object *);
+void	unref(Object *);
+void	cache(Object *);
+
+/* object sets */
+void	osinit(Objset *);
+void	osadd(Objset *, Object *);
+int	oshas(Objset *, Object *);
+Object	*osfind(Objset *, Hash);
+
+/* object listing */
+Ols	*mkols(void);
+int	olsnext(Ols *, Hash *);
+void	olsfree(Ols *);
+
+/* util functions */
+void	*emalloc(ulong);
+void	*erealloc(void *, ulong);
+char	*estrdup(char *);
+int	slurpdir(char *, Dir **);
+int	hparse(Hash *, char *);
+int	hassuffix(char *, char *);
+int	swapsuffix(char *, int, char *, char *, char *);
+char	*strip(char *);
+void	die(char *, ...);
+
+/* proto handling */
+int	readpkt(int, char*, int);
+int	writepkt(int, char*, int);
+int	flushpkt(int);
+int	parseuri(char *, char *, char *, char *, char *, char *);
+int	dialssh(char *, char *, char *, char *);
+int	dialgit(char *, char *, char *, char *);
--- /dev/null
+++ b/init
@@ -1,0 +1,41 @@
+#!/bin/rc
+
+rfork e
+
+fn usage{
+	echo git/init [-b] name >[1=2]
+	echo '	-b	init bare repository' >[1=2]
+	exit usage
+}
+
+sub='/.git'
+upstream=()
+while(~ $1 -*){
+	switch($1){
+	case '-b';
+		sub=''
+	case '-u';
+		shift
+		if(~ $#* 0)
+			usage
+		upstream=$1
+		shift
+	case *;
+		usage
+	}
+	shift
+}
+
+if (~ $#* 0)
+	dir=.
+if not if(~ $#* 1)
+	dir=$1
+if not
+	usage
+
+mkdir -p $dir$sub
+dircp /sys/lib/git/template $dir/$sub
+if(! ~ $#upstream 0){
+	echo '[remote "origin"]' >> $dir/$sub/config
+	echo '	url='$upstream >> $dir/$sub/config
+}
--- /dev/null
+++ b/log
@@ -1,0 +1,26 @@
+#!/bin/rc -e
+
+rfork en
+
+base=/mnt/git/object/
+branch=$1
+if(~ $1 '')
+	branch='master'
+if(! test -e /mnt/git/ctl)
+	git/fs
+
+commits=(`{git/query $branch})
+while(! ~$#commits 0){
+	c=$commits(1)
+	
+	echo 'Hash:	' `{cat $base/$c/hash}
+	echo 'Author:	' `{cat $base/$c/author}
+	cat $base/$c/msg | sed 's/^/	/g'
+	echo ''
+
+	commits=($commits(2-) `{cat $base/$c/parent >[2]/dev/null})
+	if(! ~ $#commits 0)
+		commits=`{mtime $base^$commits |
+			sort -rn | uniq |
+			awk -F/ '{print $NF}'}
+}
--- /dev/null
+++ b/merge
@@ -1,0 +1,37 @@
+#!/bin/rc -e
+
+rfork ne
+
+fn merge{
+	ourbr=$1/tree
+	basebr=$2/tree
+	theirbr=$3/tree
+
+	all=`{walk -f $ourbr $basebr $theirbr | \
+		 sed 's@^('$ourbr'|'$basebr'|'$theirbr')/*@@g' | sort | uniq}
+	for(f in $all){
+		if(! test -f $ourbr/$f)
+			ours=/dev/null
+		if(! test -f $basebr/$f)
+			base=/dev/null
+		if(! test -f $theirbr/$f)
+			theirs=/dev/null
+		if(! ape/diff3 -m $ourbr/$f $basebr/$f $theirbr/$f > $f)
+			echo merge needed: $f
+	}
+}
+
+fn usage{
+	echo usage: $argv0 theirs
+	exit usage
+}
+
+if(! ~ $#* 1)
+	usage
+
+git/fs
+theirs=`{git/query $1}
+ours=`{git/query HEAD}
+base=`{git/query $theirs ^ ' ' ^ $ours ^ '@'}
+
+merge /mnt/git/object/$ours /mnt/git/object/$base /mnt/git/object/$theirs
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,65 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin/git
+TARG=\
+	conf\
+	fetch\
+	fs\
+	query\
+	save\
+	send\
+	walk\
+
+RC=\
+	add\
+	branch\
+	clone\
+	commit\
+	diff\
+	init\
+	log\
+	merge\
+	pull\
+	push\
+
+OFILES=\
+	objset.$O\
+	ols.$O\
+	pack.$O\
+	proto.$O\
+	util.$O\
+	ref.$O
+
+HFILES=git.h
+
+</sys/src/cmd/mkmany
+
+# Override install target to install rc.
+install:V:
+	mkdir -p $BIN
+	for (i in $TARG)
+		mk $MKFLAGS $i.install
+	for (i in $RC)
+		mk $MKFLAGS $i.rcinstall
+	cp git.1 /sys/man/1/git
+	mk $MKFLAGS /sys/lib/git/template
+
+uninstall:V:
+	rm -rf $BIN /sys/lib/git
+
+%.c %.h: %.y
+	$YACC $YFLAGS -D1 -d -s $stem $prereq
+	mv $stem.tab.c $stem.c
+	mv $stem.tab.h $stem.h
+
+%.c %.h: %.y
+	$YACC $YFLAGS -D1 -d -s $stem $prereq
+	mv $stem.tab.c $stem.c
+	mv $stem.tab.h $stem.h
+
+%.rcinstall:V:
+	cp $stem $BIN/$stem
+
+/sys/lib/git/template: template
+	mkdir -p /sys/lib/git/template
+	dircp template /sys/lib/git/template
--- /dev/null
+++ b/objset.c
@@ -1,0 +1,66 @@
+#include <u.h>
+#include <libc.h>
+#include <pool.h>
+
+#include "git.h"
+
+void
+osinit(Objset *s)
+{
+	s->sz = 16;
+	s->nobj = 0;
+	s->obj = emalloc(s->sz * sizeof(Hash));
+}
+
+void
+osfree(Objset *s)
+{
+	free(s->obj);
+}
+
+void
+osadd(Objset *s, Object *o)
+{
+	u32int probe;
+	Object **obj;
+	int i, sz;
+
+	probe = GETBE32(o->hash.h) % s->sz;
+	while(s->obj[probe]){
+		if(hasheq(&s->obj[probe]->hash, &o->hash))
+			return;
+		probe = (probe + 1) % s->sz;
+	}
+	assert(s->obj[probe] == nil);
+	s->obj[probe] = o;
+	s->nobj++;
+	if(s->sz < 2*s->nobj){
+		sz = s->sz;
+		obj = s->obj;
+
+		s->sz *= 2;
+		s->nobj = 0;
+		s->obj = emalloc(s->sz * sizeof(Hash));
+		for(i = 0; i < sz; i++)
+			if(obj[i])
+				osadd(s, obj[i]);
+		free(obj);
+	}
+}
+
+Object*
+osfind(Objset *s, Hash h)
+{
+	u32int probe;
+
+	for(probe = GETBE32(h.h) % s->sz; s->obj[probe]; probe = (probe + 1) % s->sz)
+		if(hasheq(&s->obj[probe]->hash, &h))
+			return s->obj[probe]; 
+	return 0;
+}
+
+int
+oshas(Objset *s, Object *o)
+{
+	return osfind(s, o->hash) != nil;
+}
--- /dev/null
+++ b/ols.c
@@ -1,0 +1,170 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include "git.h"
+
+enum {
+	Sinit,
+	Siter,
+};
+
+static int
+crackidx(char *path, int *np)
+{
+	int fd;
+	char buf[4];
+
+	if((fd = open(path, OREAD)) == -1)
+		return -1;
+	if(seek(fd, 8 + 255*4, 0) == -1)
+		return -1;
+	if(readn(fd, buf, sizeof(buf)) != sizeof(buf))
+		return -1;
+	*np = GETBE32(buf);
+	return fd;
+}
+
+int
+isloosedir(char *s)
+{
+	return strlen(s) == 2 && isxdigit(s[0]) && isxdigit(s[1]);
+}
+
+int
+endswith(char *n, char *s)
+{
+	int nn, ns;
+
+	nn = strlen(n);
+	ns = strlen(s);
+	return nn > ns && strcmp(n + nn - ns, s) == 0;
+}
+
+int
+olsreadpacked(Ols *ols, Hash *h)
+{
+	char *p;
+	int i, j;
+
+	i = ols->packidx;
+	j = ols->entidx;
+
+	if(ols->state == Siter)
+		goto step;
+	for(i = 0; i < ols->npack; i++){
+		if(!endswith(ols->pack[i].name, ".idx"))
+			continue;
+		if((p = smprint(".git/objects/pack/%s", ols->pack[i].name)) == nil)
+			sysfatal("smprint: %r");
+		ols->fd = crackidx(p, &ols->nent);
+		free(p);
+		if(ols->fd == -1)
+			continue;
+		j = 0;
+		while(j < ols->nent){
+			if(readn(ols->fd, h->h, sizeof(h->h)) != sizeof(h->h))
+				continue;
+			ols->state = Siter;
+			ols->packidx = i;
+			ols->entidx = j;
+			return 0;
+step:
+			j++;
+		}
+		close(ols->fd);
+	}
+	ols->state = Sinit;
+	return -1;
+}
+
+
+int
+olsreadloose(Ols *ols, Hash *h)
+{
+	char buf[64], *p;
+	int i, j, n;
+
+	i = ols->topidx;
+	j = ols->looseidx;
+	if(ols->state == Siter)
+		goto step;
+	for(i = 0; i < ols->ntop; i++){
+		if(!isloosedir(ols->top[i].name))
+			continue;
+		if((p = smprint(".git/objects/%s", ols->top[i].name)) == nil)
+			sysfatal("smprint: %r");
+		ols->fd = open(p, OREAD);
+		free(p);
+		if(ols->fd == -1)
+			continue;
+		while((ols->nloose = dirread(ols->fd, &ols->loose)) > 0){
+			j = 0;
+			while(j < ols->nloose){
+				n = snprint(buf, sizeof(buf), "%s%s", ols->top[i].name, ols->loose[j].name);
+				if(n >= sizeof(buf))
+					goto step;
+				if(hparse(h, buf) == -1)
+					goto step;
+				ols->state = Siter;
+				ols->topidx = i;
+				ols->looseidx = j;
+				return 0;
+step:
+				j++;
+			}
+			free(ols->loose);
+			ols->loose = nil;
+		}
+		close(ols->fd);
+		ols->fd = -1;
+	}
+	ols->state = Sinit;
+	return -1;
+}
+
+Ols*
+mkols(void)
+{
+	Ols *ols;
+
+	ols = emalloc(sizeof(Ols));
+	if((ols->ntop = slurpdir(".git/objects", &ols->top)) == -1)
+		sysfatal("read top level: %r");
+	if((ols->npack = slurpdir(".git/objects/pack", &ols->pack)) == -1)
+		ols->pack = nil;
+	ols->fd = -1;
+	return ols;
+}
+
+void
+olsfree(Ols *ols)
+{
+	if(ols == nil)
+		return;
+	if(ols->fd != -1)
+		close(ols->fd);
+	free(ols->top);
+	free(ols->loose);
+	free(ols->pack);
+	free(ols);
+}
+
+int
+olsnext(Ols *ols, Hash *h)
+{
+	if(ols->stage == 0){
+		if(olsreadloose(ols, h) != -1){
+			ols->idx++;
+			return 0;
+		}
+		ols->stage++;
+	}
+	if(ols->stage == 1){
+		if(olsreadpacked(ols, h) != -1){
+			ols->idx++;
+			return 0;
+		}
+		ols->stage++;
+	}
+	return -1;
+}
--- /dev/null
+++ b/pack.c
@@ -1,0 +1,981 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+typedef struct Buf Buf;
+
+struct Buf {
+	int len;
+	int sz;
+	char *data;
+};
+
+static int	readpacked(Biobuf *, Object *, int);
+static Object	*readidxobject(Biobuf *, Hash, int);
+
+Objset objcache;
+Object *lruhead;
+Object *lrutail;
+int	ncache;
+
+static void
+clear(Object *o)
+{
+	if(!o)
+		return;
+
+	assert(o->refs == 0);
+	assert((o->flag & Ccache) == 0);
+	assert(o->flag & Cloaded);
+	switch(o->type){
+	case GCommit:
+		if(!o->commit)
+			break;
+		free(o->commit->parent);
+		free(o->commit->author);
+		free(o->commit->committer);
+		free(o->commit);
+		o->commit = nil;
+		break;
+	case GTree:
+		if(!o->tree)
+			break;
+		free(o->tree->ent);
+		free(o->tree);
+		o->tree = nil;
+		break;
+	default:
+		break;
+	}
+
+	free(o->all);
+	o->all = nil;
+	o->data = nil;
+	o->flag &= ~Cloaded;
+}
+
+void
+unref(Object *o)
+{
+	if(!o)
+		return;
+	o->refs--;
+	if(!o->refs)
+		clear(o);
+}
+
+Object*
+ref(Object *o)
+{
+	o->refs++;
+	return o;
+}
+
+void
+cache(Object *o)
+{
+	Object *p;
+
+	if(o == lruhead)
+		return;
+	if(o == lrutail)
+		lrutail = lrutail->prev;
+	if(!(o->flag & Cexist)){
+		osadd(&objcache, o);
+		o->id = objcache.nobj;
+		o->flag |= Cexist;
+	}
+	if(o->prev)
+		o->prev->next = o->next;
+	if(o->next)
+		o->next->prev = o->prev;
+	if(lrutail == o){
+		lrutail = o->prev;
+		lrutail->next = nil;
+	}else if(!lrutail)
+		lrutail = o;
+	if(lruhead)
+		lruhead->prev = o;
+	o->next = lruhead;
+	o->prev = nil;
+	lruhead = o;
+
+	if(!(o->flag & Ccache)){
+		o->flag |= Ccache;
+		ref(o);
+		ncache++;
+	}
+	while(ncache > Cachemax){
+		p = lrutail;
+		lrutail = p->prev;
+		lrutail->next = nil;
+		p->flag &= ~Ccache;
+		p->prev = nil;
+		p->next = nil;
+		unref(p);
+		ncache--;
+	}		
+}
+
+int
+bappend(void *p, void *src, int len)
+{
+	Buf *b = p;
+	char *n;
+
+	while(b->len + len >= b->sz){
+		b->sz = b->sz*2 + 64;
+		n = realloc(b->data, b->sz);
+		if(n == nil)
+			return -1;
+		b->data = n;
+	}
+	memmove(b->data + b->len, src, len);
+	b->len += len;
+	return len;
+}
+
+int
+breadc(void *p)
+{
+	return Bgetc(p);
+}
+
+int
+bdecompress(Buf *d, Biobuf *b, vlong *csz)
+{
+	vlong o;
+
+	o = Boffset(b);
+	if(inflatezlib(d, bappend, b, breadc) == -1){
+		free(d->data);
+		return -1;
+	}
+	if (csz)
+		*csz = Boffset(b) - o;
+	return d->len;
+}
+
+int
+decompress(void **p, Biobuf *b, vlong *csz)
+{
+	Buf d = {.len=0, .data=nil, .sz=0};
+
+	if(bdecompress(&d, b, csz) == -1){
+		free(d.data);
+		return -1;
+	}
+	*p = d.data;
+	return d.len;
+}
+
+static int
+preadbe32(Biobuf *b, int *v, vlong off)
+{
+	char buf[4];
+	
+	if(Bseek(b, off, 0) == -1)
+		return -1;
+	if(Bread(b, buf, sizeof(buf)) == -1)
+		return -1;
+	*v = GETBE32(buf);
+
+	return 0;
+}
+static int
+preadbe64(Biobuf *b, vlong *v, vlong off)
+{
+	char buf[8];
+	
+	if(Bseek(b, off, 0) == -1)
+		return -1;
+	if(Bread(b, buf, sizeof(buf)) == -1)
+		return -1;
+	*v = GETBE64(buf);
+	return 0;
+}
+
+int
+readvint(char *p, char **pp)
+{
+	int i, n, c;
+	
+	i = 0;
+	n = 0;
+	do {
+		c = *p++;
+		n |= (c & 0x7f) << i;
+		i += 7;
+	} while (c & 0x80);
+	*pp = p;
+
+	return n;
+}
+
+static int
+hashsearch(Hash *hlist, int nent, Hash h)
+{
+	int hi, lo, mid, d;
+
+	lo = 0;
+	hi = nent;
+	while(lo < hi){
+		mid = (lo + hi)/2;
+		d = memcmp(hlist[mid].h, h.h, sizeof h.h);
+		if(d < 0)
+			lo = mid + 1;
+		else if(d > 0)
+			hi = mid;
+		else
+			return mid;
+	}
+	return -1;
+}
+
+static int
+applydelta(Object *dst, Object *base, char *d, int nd)
+{
+	char *r, *b, *ed, *er;
+	int n, nr, c;
+	vlong o, l;
+
+	ed = d + nd;
+	b = base->data;
+	n = readvint(d, &d);
+	if(n != base->size){
+		werrstr("mismatched source size");
+		return -1;
+	}
+
+	nr = readvint(d, &d);
+	r = emalloc(nr + 64);
+	n = snprint(r, 64, "%T %d", base->type, nr) + 1;
+	dst->all = r;
+	dst->type = base->type;
+	dst->data = r + n;
+	dst->size = nr;
+	er = dst->data + nr;
+	r = dst->data;
+
+	while(1){
+		if(d == ed)
+			break;
+		c = *d++;
+		if(!c){
+			werrstr("bad delta encoding");
+			return -1;
+		}
+		/* copy from base */
+		if(c & 0x80){
+			o = 0;
+			l = 0;
+			/* Offset in base */
+			if(c & 0x01) o |= (*d++ <<  0) & 0x000000ff;
+			if(c & 0x02) o |= (*d++ <<  8) & 0x0000ff00;
+			if(c & 0x04) o |= (*d++ << 16) & 0x00ff0000;
+			if(c & 0x08) o |= (*d++ << 24) & 0xff000000;
+
+			/* Length to copy */
+			if(c & 0x10) l |= (*d++ <<  0) & 0x0000ff;
+			if(c & 0x20) l |= (*d++ <<  8) & 0x00ff00;
+			if(c & 0x40) l |= (*d++ << 16) & 0xff0000;
+			if(l == 0) l = 0x10000;
+
+			assert(o + l <= base->size);
+			memmove(r, b + o, l);
+			r += l;
+		/* inline data */
+		}else{
+			memmove(r, d, c);
+			d += c;
+			r += c;
+		}
+
+	}
+	if(r != er){
+		werrstr("truncated delta (%zd)", er - r);
+		return -1;
+	}
+
+	return nr;
+}
+
+static int
+readrdelta(Biobuf *f, Object *o, int nd, int flag)
+{
+	Object *b;
+	Hash h;
+	char *d;
+	int n;
+
+	d = nil;
+	if(Bread(f, h.h, sizeof(h.h)) != sizeof(h.h))
+		goto error;
+	if(hasheq(&o->hash, &h))
+		goto error;
+	if((n = decompress(&d, f, nil)) == -1)
+		goto error;
+	if(d == nil || n != nd)
+		goto error;
+	if((b = readidxobject(f, h, flag)) == nil)
+		goto error;
+	if(applydelta(o, b, d, n) == -1)
+		goto error;
+	free(d);
+	return 0;
+error:
+	free(d);
+	return -1;
+}
+
+static int
+readodelta(Biobuf *f, Object *o, vlong nd, vlong p, int flag)
+{
+	Object b;
+	char *d;
+	vlong r;
+	int c, n;
+
+	r = 0;
+	d = nil;
+	while(1){
+		if((c = Bgetc(f)) == -1)
+			goto error;
+		r |= c & 0x7f;
+		if (!(c & 0x80))
+			break;
+		r++;
+		r <<= 7;
+	}while(c & 0x80);
+
+	if(r > p){
+		werrstr("junk offset -%lld (from %lld)", r, p);
+		goto error;
+	}
+	if((n = decompress(&d, f, nil)) == -1)
+		goto error;
+	if(d == nil || n != nd)
+		goto error;
+	if(Bseek(f, p - r, 0) == -1)
+		goto error;
+	if(readpacked(f, &b, flag) == -1)
+		goto error;
+	if(applydelta(o, &b, d, nd) == -1)
+		goto error;
+	free(d);
+	return 0;
+error:
+	free(d);
+	return -1;
+}
+
+static int
+readpacked(Biobuf *f, Object *o, int flag)
+{
+	int c, s, n;
+	vlong l, p;
+	Type t;
+	Buf b;
+
+	p = Boffset(f);
+	c = Bgetc(f);
+	if(c == -1)
+		return -1;
+	l = c & 0xf;
+	s = 4;
+	t = (c >> 4) & 0x7;
+	if(!t){
+		werrstr("unknown type for byte %x", c);
+		return -1;
+	}
+	while(c & 0x80){
+		if((c = Bgetc(f)) == -1)
+			return -1;
+		l |= (c & 0x7f) << s;
+		s += 7;
+	}
+
+	switch(t){
+	default:
+		werrstr("invalid object at %lld", Boffset(f));
+		return -1;
+	case GCommit:
+	case GTree:
+	case GTag:
+	case GBlob:
+		b.sz = 64 + l;
+
+		b.data = emalloc(b.sz);
+		n = snprint(b.data, 64, "%T %lld", t, l) + 1;
+		b.len = n;
+		if(bdecompress(&b, f, nil) == -1){
+			free(b.data);
+			return -1;
+		}
+		o->type = t;
+		o->all = b.data;
+		o->data = b.data + n;
+		o->size = b.len - n;
+		break;
+	case GOdelta:
+		if(readodelta(f, o, l, p, flag) == -1)
+			return -1;
+		break;
+	case GRdelta:
+		if(readrdelta(f, o, l, flag) == -1)
+			return -1;
+		break;
+	}
+	o->flag |= Cloaded|flag;
+	return 0;
+}
+
+static int
+readloose(Biobuf *f, Object *o, int flag)
+{
+	struct { char *tag; int type; } *p, types[] = {
+		{"blob", GBlob},
+		{"tree", GTree},
+		{"commit", GCommit},
+		{"tag", GTag},
+		{nil},
+	};
+	char *d, *s, *e;
+	vlong sz, n;
+	int l;
+
+	n = decompress(&d, f, nil);
+	if(n == -1)
+		return -1;
+
+	s = d;
+	o->type = GNone;
+	for(p = types; p->tag; p++){
+		l = strlen(p->tag);
+		if(strncmp(s, p->tag, l) == 0){
+			s += l;
+			o->type = p->type;
+			while(!isspace(*s))
+				s++;
+			break;
+		}
+	}
+	if(o->type == GNone){
+		free(o->data);
+		return -1;
+	}
+	sz = strtol(s, &e, 0);
+	if(e == s || *e++ != 0){
+		werrstr("malformed object header");
+		goto error;
+	}
+	if(sz != n - (e - d)){
+		werrstr("mismatched sizes");
+		goto error;
+	}
+	o->size = sz;
+	o->data = e;
+	o->all = d;
+	o->flag |= Cloaded|flag;
+	return 0;
+
+error:
+	free(d);
+	return -1;
+}
+
+vlong
+searchindex(Biobuf *f, Hash h)
+{
+	int lo, hi, idx, i, nent;
+	vlong o, oo;
+	Hash hh;
+
+	o = 8;
+	/*
+	 * Read the fanout table. The fanout table
+	 * contains 256 entries, corresponsding to
+	 * the first byte of the hash. Each entry
+	 * is a 4 byte big endian integer, containing
+	 * the total number of entries with a leading
+	 * byte <= the table index, allowing us to
+	 * rapidly do a binary search on them.
+	 */
+	if (h.h[0] == 0){
+		lo = 0;
+		if(preadbe32(f, &hi, o) == -1)
+			goto err;
+	} else {
+		o += h.h[0]*4 - 4;
+		if(preadbe32(f, &lo, o + 0) == -1)
+			goto err;
+		if(preadbe32(f, &hi, o + 4) == -1)
+			goto err;
+	}
+	if(hi == lo)
+		goto notfound;
+	if(preadbe32(f, &nent, 8 + 255*4) == -1)
+		goto err;
+
+	/*
+	 * Now that we know the range of hashes that the
+	 * entry may exist in, read them in so we can do
+	 * a bsearch.
+	 */
+	idx = -1;
+	Bseek(f, Hashsz*lo + 8 + 256*4, 0);
+	for(i = 0; i < hi - lo; i++){
+		if(Bread(f, hh.h, sizeof(hh.h)) == -1)
+			goto err;
+		if(hasheq(&hh, &h))
+			idx = lo + i;
+	}
+	if(idx == -1)
+		goto notfound;
+
+
+	/*
+	 * We found the entry. If it's 32 bits, then we
+	 * can just return the oset, otherwise the 32
+	 * bit entry contains the oset to the 64 bit
+	 * entry.
+	 */
+	oo = 8;			/* Header */
+	oo += 256*4;		/* Fanout table */
+	oo += Hashsz*nent;	/* Hashes */
+	oo += 4*nent;		/* Checksums */
+	oo += 4*idx;		/* Offset offset */
+	if(preadbe32(f, &i, oo) == -1)
+		goto err;
+	o = i & 0xffffffff;
+	if(o & (1ull << 31)){
+		o &= 0x7fffffff;
+		if(preadbe64(f, &o, o) == -1)
+			goto err;
+	}
+	return o;
+
+err:
+	fprint(2, "unable to read packfile: %r\n");
+	return -1;
+notfound:
+	werrstr("not present: %H", h);
+	return -1;		
+}
+
+/*
+ * Scans for non-empty word, copying it into buf.
+ * Strips off word, leading, and trailing space
+ * from input.
+ * 
+ * Returns -1 on empty string or error, leaving
+ * input unmodified.
+ */
+static int
+scanword(char **str, int *nstr, char *buf, int nbuf)
+{
+	char *p;
+	int n, r;
+
+	r = -1;
+	p = *str;
+	n = *nstr;
+	while(n && isblank(*p)){
+		n--;
+		p++;
+	}
+
+	for(; n && *p && !isspace(*p); p++, n--){
+		r = 0;
+		*buf++ = *p;
+		nbuf--;
+		if(nbuf == 0)
+			return -1;
+	}
+	while(n && isblank(*p)){
+		n--;
+		p++;
+	}
+	*buf = 0;
+	*str = p;
+	*nstr = n;
+	return r;
+}
+
+static void
+nextline(char **str, int *nstr)
+{
+	char *s;
+
+	if((s = strchr(*str, '\n')) != nil){
+		*nstr -= s - *str + 1;
+		*str = s + 1;
+	}
+}
+
+static int
+parseauthor(char **str, int *nstr, char **name, vlong *time)
+{
+	char buf[128];
+	Resub m[4];
+	char *p;
+	int n, nm;
+
+	if((p = strchr(*str, '\n')) == nil)
+		sysfatal("malformed author line");
+	n = p - *str;
+	if(n >= sizeof(buf))
+		sysfatal("overlong author line");
+	memset(m, 0, sizeof(m));
+	snprint(buf, n + 1, *str);
+	*str = p;
+	*nstr -= n;
+	
+	if(!regexec(authorpat, buf, m, nelem(m)))
+		sysfatal("invalid author line %s", buf);
+	nm = m[1].ep - m[1].sp;
+	*name = emalloc(nm + 1);
+	memcpy(*name, m[1].sp, nm);
+	buf[nm] = 0;
+	
+	nm = m[2].ep - m[2].sp;
+	memcpy(buf, m[2].sp, nm);
+	buf[nm] = 0;
+	*time = atoll(buf);
+	return 0;
+}
+
+static void
+parsecommit(Object *o)
+{
+	char *p, *t, buf[128];
+	int np;
+
+	p = o->data;
+	np = o->size;
+	o->commit = emalloc(sizeof(Cinfo));
+	while(1){
+		if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+			break;
+		if(strcmp(buf, "tree") == 0){
+			if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+				sysfatal("invalid commit: tree missing");
+			if(hparse(&o->commit->tree, buf) == -1)
+				sysfatal("invalid commit: garbled tree");
+		}else if(strcmp(buf, "parent") == 0){
+			if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+				sysfatal("invalid commit: missing parent");
+			o->commit->parent = realloc(o->commit->parent, ++o->commit->nparent * sizeof(Hash));
+			if(!o->commit->parent)
+				sysfatal("unable to malloc: %r");
+			if(hparse(&o->commit->parent[o->commit->nparent - 1], buf) == -1)
+				sysfatal("invalid commit: garbled parent");
+		}else if(strcmp(buf, "author") == 0){
+			parseauthor(&p, &np, &o->commit->author, &o->commit->mtime);
+		}else if(strcmp(buf, "committer") == 0){
+			parseauthor(&p, &np, &o->commit->committer, &o->commit->ctime);
+		}else if(strcmp(buf, "gpgsig") == 0){
+			/* just drop it */
+			if((t = strstr(p, "-----END PGP SIGNATURE-----")) == nil)
+				sysfatal("malformed gpg signature");
+			np -= t - p;
+			p = t;
+		}
+		nextline(&p, &np);
+	}
+	while (np && isspace(*p)) {
+		p++;
+		np--;
+	}
+	o->commit->msg = p;
+	o->commit->nmsg = np;
+}
+
+static void
+parsetree(Object *o)
+{
+	char *p, buf[256];
+	int np, nn, m;
+	Dirent *t;
+
+	p = o->data;
+	np = o->size;
+	o->tree = emalloc(sizeof(Tinfo));
+	while(np > 0){
+		if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+			break;
+		o->tree->ent = erealloc(o->tree->ent, ++o->tree->nent * sizeof(Dirent));
+		t = &o->tree->ent[o->tree->nent - 1];
+		memset(t, 0, sizeof(Dirent));
+		m = strtol(buf, nil, 8);
+		/* FIXME: symlinks and other BS */
+		if(m == 0160000){
+			print("setting mode to link...\n");
+			t->mode |= DMDIR;
+			t->modref = 1;
+		}
+		t->mode = m & 0777;
+		if(m & 0040000)
+			t->mode |= DMDIR;
+		t->name = p;
+		nn = strlen(p) + 1;
+		p += nn;
+		np -= nn;
+		if(np < sizeof(t->h.h))
+			sysfatal("malformed tree %H, remaining %d (%s)", o->hash, np, p);
+		memcpy(t->h.h, p, sizeof(t->h.h));
+		p += sizeof(t->h.h);
+		np -= sizeof(t->h.h);
+	}
+}
+
+static void
+parsetag(Object *)
+{
+}
+
+void
+parseobject(Object *o)
+{
+	if(o->flag & Cparsed)
+		return;
+	switch(o->type){
+	case GTree:	parsetree(o);	break;
+	case GCommit:	parsecommit(o);	break;
+	case GTag:	parsetag(o);	break;
+	default:	break;
+	}
+	o->flag |= Cparsed;
+}
+
+static Object*
+readidxobject(Biobuf *idx, Hash h, int flag)
+{
+	char path[Pathmax];
+	char hbuf[41];
+	Biobuf *f;
+	Object *obj;
+	int l, i, n;
+	vlong o;
+	Dir *d;
+
+	USED(idx);
+	if((obj = osfind(&objcache, h)) != nil){
+		if(obj->flag & Cloaded)
+			return obj;
+		if(obj->flag & Cidx){
+			assert(idx != nil);
+			o = Boffset(idx);
+			if(Bseek(idx, obj->off, 0) == -1)
+				sysfatal("could not seek to object offset");
+			if(readpacked(idx, obj, flag) == -1)
+				sysfatal("could not reload object %H", obj->hash);
+			if(Bseek(idx, o, 0) == -1)
+				sysfatal("could not restore offset");
+			cache(obj);
+			return obj;
+		}
+	}
+
+	d = nil;
+	obj = emalloc(sizeof(Object));
+	obj->id = objcache.nobj + 1;
+	obj->hash = h;
+
+	snprint(hbuf, sizeof(hbuf), "%H", h);
+	snprint(path, sizeof(path), ".git/objects/%c%c/%s", hbuf[0], hbuf[1], hbuf + 2);
+	if((f = Bopen(path, OREAD)) != nil){
+		if(readloose(f, obj, flag) == -1)
+			goto error;
+		Bterm(f);
+		parseobject(obj);
+		cache(obj);
+		return obj;
+	}
+
+	if ((n = slurpdir(".git/objects/pack", &d)) == -1)
+		goto error;
+	o = -1;
+	for(i = 0; i < n; i++){
+		l = strlen(d[i].name);
+		if(l > 4 && strcmp(d[i].name + l - 4, ".idx") != 0)
+			continue;
+		snprint(path, sizeof(path), ".git/objects/pack/%s", d[i].name);
+		if((f = Bopen(path, OREAD)) == nil)
+			continue;
+		o = searchindex(f, h);
+		Bterm(f);
+		if(o == -1)
+			continue;
+		break;
+	}
+
+	if (o == -1)
+		goto error;
+
+	if((n = snprint(path, sizeof(path), "%s", path)) >= sizeof(path) - 4)
+		goto error;
+	memcpy(path + n - 4, ".pack", 6);
+	if((f = Bopen(path, OREAD)) == nil)
+		goto error;
+	if(Bseek(f, o, 0) == -1)
+		goto error;
+	if(readpacked(f, obj, flag) == -1)
+		goto error;
+	Bterm(f);
+	parseobject(obj);
+	cache(obj);
+	return obj;
+error:
+	free(d);
+	free(obj);
+	return nil;
+}
+
+Object*
+readobject(Hash h)
+{
+	Object *o;
+
+	o = readidxobject(nil, h, 0);
+	if(o)
+		ref(o);
+	return o;
+}
+
+int
+objcmp(void *pa, void *pb)
+{
+	Object *a, *b;
+
+	a = *(Object**)pa;
+	b = *(Object**)pb;
+	return memcmp(a->hash.h, b->hash.h, sizeof(a->hash.h));
+}
+
+static int
+hwrite(Biobuf *b, void *buf, int len, DigestState **st)
+{
+	*st = sha1(buf, len, nil, *st);
+	return Bwrite(b, buf, len);
+}
+
+int
+indexpack(char *pack, char *idx, Hash ph)
+{
+	char hdr[4*3], buf[8];
+	int nobj, nvalid, nbig, n, i, step;
+	Object *o, **objects;
+	DigestState *st;
+	char *valid;
+	Biobuf *f;
+	Hash h;
+	int c;
+
+	if((f = Bopen(pack, OREAD)) == nil)
+		return -1;
+	if(Bread(f, hdr, sizeof(hdr)) != sizeof(hdr)){
+		werrstr("short read on header");
+		return -1;
+	}
+	if(memcmp(hdr, "PACK\0\0\0\2", 8) != 0){
+		werrstr("invalid header");
+		return -1;
+	}
+
+	nvalid = 0;
+	nobj = GETBE32(hdr + 8);
+	objects = calloc(nobj, sizeof(Object*));
+	valid = calloc(nobj, sizeof(char));
+	step = nobj/100;
+	if(!step)
+		step++;
+	while(nvalid != nobj){
+		fprint(2, "indexing (%d/%d):", nvalid, nobj);
+		n = 0;
+		for(i = 0; i < nobj; i++){
+			if(valid[i]){
+				n++;
+				continue;
+			}
+			if(i % step == 0)
+				fprint(2, ".");
+			if(!objects[i]){
+				o = emalloc(sizeof(Object));
+				o->off = Boffset(f);
+				objects[i] = o;
+			}
+			o = objects[i];
+			Bseek(f, o->off, 0);
+			if (readpacked(f, o, Cidx) == 0){
+				sha1((uchar*)o->all, o->size + strlen(o->all) + 1, o->hash.h, nil);
+				cache(o);
+				valid[i] = 1;
+				n++;
+			}
+		}
+		fprint(2, "\n");
+		if(n == nvalid){
+			sysfatal("fix point reached too early: %d/%d: %r", nvalid, nobj);
+			goto error;
+		}
+		nvalid = n;
+	}
+	Bterm(f);
+
+	st = nil;
+	qsort(objects, nobj, sizeof(Object*), objcmp);
+	if((f = Bopen(idx, OWRITE)) == nil)
+		return -1;
+	if(Bwrite(f, "\xfftOc\x00\x00\x00\x02", 8) != 8)
+		goto error;
+	/* fanout table */
+	c = 0;
+	for(i = 0; i < 256; i++){
+		while(c < nobj && (objects[c]->hash.h[0] & 0xff) <= i)
+			c++;
+		PUTBE32(buf, c);
+		hwrite(f, buf, 4, &st);
+	}
+	for(i = 0; i < nobj; i++){
+		o = objects[i];
+		hwrite(f, o->hash.h, sizeof(o->hash.h), &st);
+	}
+
+	/* fuck it, pointless */
+	for(i = 0; i < nobj; i++){
+		PUTBE32(buf, 42);
+		hwrite(f, buf, 4, &st);
+	}
+
+	nbig = 0;
+	for(i = 0; i < nobj; i++){
+		if(objects[i]->off <= (1ull<<31))
+			PUTBE32(buf, objects[i]->off);
+		else
+			PUTBE32(buf, (1ull << 31) | nbig++);
+		hwrite(f, buf, 4, &st);
+	}
+	for(i = 0; i < nobj; i++){
+		if(objects[i]->off > (1ull<<31)){
+			PUTBE64(buf, objects[i]->off);
+			hwrite(f, buf, 8, &st);
+		}
+	}
+	hwrite(f, ph.h, sizeof(ph.h), &st);
+	sha1(nil, 0, h.h, st);
+	Bwrite(f, h.h, sizeof(h.h));
+
+	free(objects);
+	free(valid);
+	Bterm(f);
+	return 0;
+
+error:
+	free(objects);
+	free(valid);
+	Bterm(f);
+	return -1;
+}
--- /dev/null
+++ b/proto.c
@@ -1,0 +1,161 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+int chattygit;
+
+int
+readpkt(int fd, char *buf, int nbuf)
+{
+	char len[5];
+	char *e;
+	int n;
+
+	if(readn(fd, len, 4) == -1)
+		return -1;
+	len[4] = 0;
+	n = strtol(len, &e, 16);
+	if(n == 0){
+		if(chattygit)
+			fprint(2, "readpkt: 0000\n");
+		return 0;
+	}
+	if(e != len + 4 || n <= 4)
+		sysfatal("invalid packet line length");
+	n  -= 4;
+	if(n >= nbuf)
+		sysfatal("buffer too small");
+	if(readn(fd, buf, n) != n)
+		return -1;
+	buf[n] = 0;
+	if(chattygit)
+		fprint(2, "readpkt: %s:\t%.*s\n", len, nbuf, buf);
+	return n;
+}
+
+int
+writepkt(int fd, char *buf, int nbuf)
+{
+	char len[5];
+
+
+	snprint(len, sizeof(len), "%04x", nbuf + 4);
+	if(write(fd, len, 4) != 4)
+		return -1;
+	if(write(fd, buf, nbuf) != nbuf)
+		return -1;
+	if(chattygit){
+		fprint(2, "writepkt: %s:\t", len);
+		write(2, buf, nbuf);
+		write(2, "\n", 1);
+	}
+	return 0;
+}
+
+int
+flushpkt(int fd)
+{
+	if(chattygit)
+		fprint(2, "writepkt: 0000\n");
+	return write(fd, "0000", 4);
+}
+
+static void
+grab(char *dst, int n, char *p, char *e)
+{
+	int l;
+
+	l = e - p;
+	if(l >= n)
+		sysfatal("overlong component");
+	memcpy(dst, p, l);
+	dst[l + 1] = 0;
+
+}
+
+int
+parseuri(char *uri, char *proto, char *host, char *port, char *path, char *repo)
+{
+	char *s, *p, *q;
+	int n;
+
+	p = strstr(uri, "://");
+	if(!p){
+		werrstr("missing protocol");
+		return -1;
+	}
+	grab(proto, Nproto, uri, p);
+	s = p + 3;
+
+	p = strstr(s, "/");
+	if(!p || strlen(p) == 1){
+		werrstr("missing path");
+		return -1;
+	}
+	q = memchr(s, ':', p - s);
+	if(q){
+		grab(host, Nhost, s, q);
+		grab(port, Nport, q + 1, p);
+	}else{
+		grab(host, Nhost, s, p);
+		snprint(port, Nport, "9418");
+	}
+	
+	snprint(path, Npath, "%s", p);
+	p = strrchr(p, '/') + 1;
+	if(!p || strlen(p) == 0){
+		werrstr("missing repository in uri");
+		return -1;
+	}
+	n = strlen(p);
+	if(hassuffix(p, ".git"))
+		n -= 4;
+	grab(repo, Nrepo, p, p + n);
+	return 0;
+}
+
+int
+dialssh(char *host, char *, char *path, char *direction)
+{
+	int pid, pfd[2];
+	char cmd[64];
+
+	print("dialing via ssh %s...\n", host);
+	if(pipe(pfd) == -1)
+		sysfatal("unable to open pipe: %r");
+	pid = fork();
+	if(pid == -1)
+		sysfatal("unable to fork");
+	if(pid == 0){
+		close(pfd[1]);
+		dup(pfd[0], 0);
+		dup(pfd[0], 1);
+		snprint(cmd, sizeof(cmd), "git-%s-pack", direction);
+		execl("/bin/ssh", "ssh", host, cmd, path, nil);
+	}else{
+		close(pfd[0]);
+		return pfd[1];
+	}
+	return -1;
+}
+
+int
+dialgit(char *host, char *port, char *path, char *direction)
+{
+	char *ds, cmd[128];
+	int fd, l;
+
+	ds = netmkaddr(host, "tcp", port);
+	print("dialing %s...\n", ds);
+	fd = dial(ds, nil, nil, nil);
+	if(fd == -1)
+		return -1;
+	l = snprint(cmd, sizeof(cmd), "git-%s-pack %s\n", direction, path);
+	if(writepkt(fd, cmd, l + 1) == -1){
+		print("failed to write message\n");
+		close(fd);
+		return -1;
+	}
+	return fd;
+}
--- /dev/null
+++ b/pull
@@ -1,0 +1,96 @@
+#!/bin/rc -e
+
+rfork en
+
+nl='
+'
+
+fn update{
+	update=$1
+	branch=$2
+	upstream=$3
+	url=$4
+	dir=$5
+	
+	{git/fetch -b $branch -u $upstream $url >[2=3] | awk -v 'update='^$update '
+		function writeref(ref, hash)
+		{
+			outfile = ".git/"ref
+			system("mkdir -p `{basename -d "outfile"}")
+			print hash > outfile
+			close(outfile)
+		}
+
+		/^remote/{
+			if($2=="HEAD")
+				next
+
+			if(update)
+				writeref($2, $3)
+			gsub("^refs/heads", "refs/remotes/'$remote'", $2)
+			writeref($2, $3)
+		}
+	'} |[3] tr '\x0d' '\x0a'
+}
+
+fn usage{
+	echo 'usage: $argv0 [-a] [-u upstream] [-b branch]' >[1=2]
+	echo '	-u up:	pull from upstream "up" (default: origin)' >[1=2]
+	echo '	-f:	fetch without updating working copy' >[1=2]
+	exit usage
+}
+
+git/fs
+branch=`{awk '$1=="branch"{print $2}' < /mnt/git/ctl}
+remote=()
+update='true'
+upstream=origin
+while(~ $1 -*){
+	switch($1){
+	case -u;
+		shift
+		remote=$1
+		upstream=SOMEONE
+	case -b:
+		shift
+		branch=$1
+	case -f;
+		update=''
+	case *;
+		usage
+	}
+	shift
+}
+
+if(! ~ $#* 0)
+	usage
+if(~ $#remote 0)
+	remote=`{git/conf 'remote "'$upstream'".url'}
+if(~ $#remote 0){
+	echo 'no idea from where to pull'
+	exit upstream
+}
+
+if(! cd `{git/conf -r})
+	exit 'not in git repository'
+
+dir=/mnt/git/branch/$branch/tree
+if(! git/walk -q){
+	echo $status
+	echo 'repository is dirty: commit before pulling' >[1=2]
+	exit 'dirty'
+}
+oldfiles=`$nl{git/walk -cfT}
+update $update  $branch $upstream $remote $dir
+if(! ~ $update 0){
+	rm -f $oldfiles
+	tree=/mnt/git/HEAD/tree
+	@{builtin cd $tree && tar cif /fd/1 .} | @{tar xf /fd/0}
+	for(f in `$nl{walk -f $tree | sed 's@^'$tree'/*@@'}){
+		if(! ~ $#f 0){
+			idx=.git/index9/tracked/$f
+			mkdir -p `{basename -d $idx}
+			walk -eq $f > $idx
+		}
+	}
+}
--- /dev/null
+++ b/push
@@ -1,0 +1,53 @@
+#!/bin/rc
+
+rfork en
+
+if(! cd `{git/conf -r})
+	exit 'not in git repository'
+
+git/fs
+fn usage {
+	echo 'usage: git/push [-a] [-u upstream] [-b branch] [-r rmbranch]' >[1=2]
+	echo '	-a:		push all' >[1=2]
+	echo '	-u upstream:	push to repo "upstream" (default: origin)' >[1=2]
+	echo '	-b branch:	push branch "branch" (default: current branch)' >[1=2]
+	exit usage
+}
+
+remote=()
+sendall=''
+remove=()
+upstream='origin'
+branch=`{awk '$1=="branch"{print $2}' < /mnt/git/ctl}
+while(~ $1 -* && ! ~ $1 --){
+	switch($1){
+	case -u;
+		shift
+		upstream=$1
+	case -a;
+		sendall=true
+	case -b;
+		shift
+		branch=$1
+	case -r;
+		shift
+		remove=(-r$1 $remove);
+	case *; usage
+	}
+	shift
+}
+
+if(! ~ $#* 0)
+	usage
+if(~ $#remote 0)
+	remote=`{git/conf 'remote "'$upstream'".url'}
+if(~ $#remote 0)
+	remote=$upstream
+if(~ $#remote 0){
+	echo 'no idea where to push'
+	exit upstream
+}
+if(~ $sendall '')
+	git/send -b $branch  $remove $remote
+if not
+	git/send  $remove -a $remote
--- /dev/null
+++ b/query.c
@@ -1,0 +1,36 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+int fullpath;
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-p]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, j, n;
+	Hash *h;
+
+	ARGBEGIN{
+	case 'p':	fullpath++;	break;
+	default:	usage();	break;
+	}ARGEND;
+
+	gitinit();
+	for(i = 0; i < argc; i++){
+		if((n = resolverefs(&h, argv[i])) == -1)
+			sysfatal("resolve %s: %r", argv[i]);
+		for(j = 0; j < n; j++)
+			if(fullpath)
+				print("/mnt/git/object/%H\n", h[j]);
+			else
+				print("%H\n", h[j]);
+	}
+}
--- /dev/null
+++ b/ref.c
@@ -1,0 +1,422 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+typedef struct Eval	Eval;
+typedef struct XObject	XObject;
+
+struct Eval {
+	char	*str;
+	char	*p;
+	Object	**stk;
+	int	nstk;
+	int	stksz;
+};
+
+struct XObject {
+	Object	*obj;
+	Object	*mark;
+	XObject	*queue;
+	XObject	*next;
+};
+
+
+void
+eatspace(Eval *ev)
+{
+	while(isspace(ev->p[0]))
+		ev->p++;
+}
+
+int
+objdatecmp(void *pa, void *pb)
+{
+	Object *a, *b;
+
+	a = *(Object**)pa;
+	b = *(Object**)pb;
+	assert(a->type == GCommit && b->type == GCommit);
+	if(a->commit->mtime == b->commit->mtime)
+		return 0;
+	else if(a->commit->mtime < b->commit->mtime)
+		return -1;
+	else
+		return 1;
+}
+
+void
+push(Eval *ev, Object *o)
+{
+	if(ev->nstk == ev->stksz){
+		ev->stksz = 2*ev->stksz + 1;
+		ev->stk = erealloc(ev->stk, ev->stksz*sizeof(Object*));
+	}
+	ev->stk[ev->nstk++] = o;
+}
+
+Object*
+pop(Eval *ev)
+{
+	if(ev->nstk == 0)
+		sysfatal("stack underflow");
+	return ev->stk[--ev->nstk];
+}
+
+Object*
+peek(Eval *ev)
+{
+	if(ev->nstk == 0)
+		sysfatal("stack underflow");
+	return ev->stk[ev->nstk - 1];
+}
+
+int
+word(Eval *ev, char *b, int nb)
+{
+	char *p, *e;
+	int n;
+
+	p = ev->p;
+	for(e = p; isalnum(*e) || *e == '/'; e++)
+		/* nothing */;
+	/* 1 for nul terminator */
+	n = e - p + 1;
+	if(n >= nb)
+		n = nb;
+	snprint(b, n, "%s", p);
+	ev->p = e;
+	return n > 0;
+}
+
+int
+take(Eval *ev, char *m)
+{
+	int l;
+
+	l = strlen(m);
+	if(strncmp(ev->p, m, l) != 0)
+		return 0;
+	ev->p += l;
+	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 = malloc(sizeof(*h));
+	h->obj = o;
+	h->mark = nil;
+	h->queue = nil;
+	h->next = ht[hh];
+	ht[hh] = h;
+	return h;
+}
+
+int
+ancestor(Eval *ev)
+{
+	Object *a, *b, *o, *p;
+	XObject *ht[256];
+	XObject *h, *q, *q1, *q2;
+	int i, r;
+
+	if(ev->nstk < 2){
+		werrstr("ancestor needs 2 objects");
+		return -1;
+	}
+	a = pop(ev);
+	b = pop(ev);
+	if(a == b){
+		push(ev, a);
+		return 0;
+	}
+
+	r = -1;
+	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]);
+				h = hnode(ht, p);
+				if(h->mark != nil){
+					if(h->mark != q->mark){
+						push(ev, h->obj);
+						r = 0;
+						goto done;
+					}
+				} else {
+					h->mark = q->mark;
+					h->queue = q2;
+					q2 = h;
+				}
+			}
+		}
+		if(q2 == nil){
+			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
+parent(Eval *ev)
+{
+	Object *o, *p;
+
+	o = pop(ev);
+	/* Special case: first commit has no parent. */
+	if(o->commit->nparent == 0 || (p = readobject(o->commit->parent[0])) == nil){
+		werrstr("no parent for %H", o->hash);
+		return -1;
+	}
+	push(ev, p);
+	return 0;
+}
+
+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])){
+			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;
+		}
+	}
+	return -1;
+}
+
+int
+range(Eval *ev)
+{
+	Object *a, *b, *p, **all;
+	int nall, *idx, mark;
+	Objset keep, skip;
+
+	b = pop(ev);
+	a = pop(ev);
+	if(a->type != GCommit || b->type != GCommit){
+		werrstr("non-commit object in range");
+		return -1;
+	}
+
+	p = b;
+	all = nil;
+	idx = nil;
+	nall = 0;
+	mark = ev->nstk;
+	osinit(&keep);
+	osinit(&skip);
+	while(1){
+		all = erealloc(all, (nall + 1)*sizeof(Object*));
+		idx = erealloc(idx, (nall + 1)*sizeof(int));
+		all[nall] = p;
+		idx[nall] = 0;
+		if(p == a)
+			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))
+			if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1)
+				break;
+		else if(oshas(&skip, p))
+			if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1)
+				break;
+
+		if((p = readobject(p->commit->parent[idx[nall]])) == nil)
+			sysfatal("bad commit %H", p->commit->parent[idx[nall]]);
+		nall++;
+	}
+	free(all);
+	qsort(ev->stk + mark, ev->nstk - mark, sizeof(Object*), objdatecmp);
+	return 0;
+}
+
+int
+readref(Hash *h, char *ref)
+{
+	static char *try[] = {"", "refs/", "refs/heads/", "refs/remotes/", "refs/tags/", nil};
+	char buf[256], s[256], **pfx;
+	int r, f, n;
+
+	/* TODO: support hash prefixes */
+	if((r = hparse(h, ref)) != -1)
+		return r;
+	if(strcmp(ref, "HEAD") == 0){
+		snprint(buf, sizeof(buf), ".git/HEAD");
+		if((f = open(buf, OREAD)) == -1)
+			return -1;
+		if((n = readn(f, s, sizeof(s) - 1))== -1)
+			return -1;
+		s[n] = 0;
+		strip(s);
+		r = hparse(h, s);
+		goto found;
+	}
+	for(pfx = try; *pfx; pfx++){
+		snprint(buf, sizeof(buf), ".git/%s%s", *pfx, ref);
+		if((f = open(buf, OREAD)) == -1)
+			continue;
+		if((n = readn(f, s, sizeof(s) - 1)) == -1)
+			continue;
+		s[n] = 0;
+		strip(s);
+		r = hparse(h, s);
+		close(f);
+		goto found;
+	}
+	return -1;
+
+found:
+	if(r == -1 && strstr(s, "ref: ") == s)
+		r = readref(h, s + strlen("ref: "));
+	return r;
+}
+
+int
+evalpostfix(Eval *ev)
+{
+	char name[256];
+	Object *o;
+	Hash h;
+
+	eatspace(ev);
+	if(!word(ev, name, sizeof(name))){
+		werrstr("expected name in expression");
+		return -1;
+	}
+	if(readref(&h, name) == -1){
+		werrstr("could not resolve ref %s", name);
+		return -1;
+	}else if((o = readobject(h)) == nil){
+		werrstr("invalid ref %s (hash %H)", name, h);
+		return -1;
+	}
+	push(ev, o);
+
+	while(1){
+		eatspace(ev);
+		switch(ev->p[0]){
+		case '^':
+			ev->p++;
+			if(parent(ev) == -1)
+				return -1;
+			break;
+		case '@':
+			ev->p++;
+			if(ancestor(ev) == -1)
+				return -1;
+			break;
+		default:
+			goto done;
+			break;
+		}	
+	}
+done:
+	return 0;
+}
+
+int
+evalexpr(Eval *ev, char *ref)
+{
+	memset(ev, 0, sizeof(*ev));
+	ev->str = ref;
+	ev->p = ref;
+
+	while(1){
+		if(evalpostfix(ev) == -1)
+			return -1;
+		if(ev->p[0] == '\0')
+			return 0;
+		else if(take(ev, ":") || take(ev, "..")){
+			if(evalpostfix(ev) == -1)
+				return -1;
+			if(ev->p[0] != '\0'){
+				werrstr("junk at end of expression");
+				return -1;
+			}
+			return range(ev);
+		}
+	}
+}
+
+int
+resolverefs(Hash **r, char *ref)
+{
+	Eval ev;
+	Hash *h;
+	int i;
+
+	if(evalexpr(&ev, ref) == -1){
+		free(ev.stk);
+		return -1;
+	}
+	h = emalloc(ev.nstk*sizeof(Hash));
+	for(i = 0; i < ev.nstk; i++)
+		h[i] = ev.stk[i]->hash;
+	*r = h;
+	return ev.nstk;
+}
+
+int
+resolveref(Hash *r, char *ref)
+{
+	Eval ev;
+
+	if(evalexpr(&ev, ref) == -1){
+		free(ev.stk);
+		return -1;
+	}
+	if(ev.nstk != 1){
+		werrstr("ambiguous ref expr");
+		free(ev.stk);
+		return -1;
+	}
+	*r = ev.stk[0]->hash;
+	return 0;
+}
--- /dev/null
+++ b/save.c
@@ -1,0 +1,273 @@
+#include <u.h>
+#include <libc.h>
+#include "git.h"
+
+typedef struct Objbuf Objbuf;
+struct Objbuf {
+	int off;
+	char *hdr;
+	int nhdr;
+	char *dat;
+	int ndat;
+};
+enum {
+	Maxparents = 16,
+};
+
+static int
+bwrite(void *p, void *buf, int nbuf)
+{
+	return Bwrite(p, buf, nbuf);
+}
+
+static int
+objbytes(void *p, void *buf, int nbuf)
+{
+	Objbuf *b;
+	int r, n, o;
+	char *s;
+
+	b = p;
+	n = 0;
+	if(b->off < b->nhdr){
+		r = b->nhdr - b->off;
+		r = (nbuf < r) ? nbuf : r;
+		memcpy(buf, b->hdr, r);
+		b->off += r;
+		nbuf -= r;
+		n += r;
+	}
+	if(b->off < b->ndat + b->nhdr){
+		s = buf;
+		o = b->off - b->nhdr;
+		r = b->ndat - o;
+		r = (nbuf < r) ? nbuf : r;
+		memcpy(s + n, b->dat + o, r);
+		b->off += r;
+		n += r;
+	}
+	return n;
+}
+
+void
+writeobj(Hash *h, char *hdr, int nhdr, char *dat, int ndat)
+{
+	Objbuf b = {.off=0, .hdr=hdr, .nhdr=nhdr, .dat=dat, .ndat=ndat};
+	char s[64], o[256];
+	SHA1state *st;
+	Biobuf *f;
+	int fd;
+
+	st = sha1((uchar*)hdr, nhdr, nil, nil);
+	st = sha1((uchar*)dat, ndat, nil, st);
+	sha1(nil, 0, h->h, st);
+	snprint(s, sizeof(s), "%H", *h);
+	fd = create(".git/objects", OREAD, DMDIR|0755);
+	close(fd);
+	snprint(o, sizeof(o), ".git/objects/%c%c", s[0], s[1]);
+	fd = create(o, OREAD, DMDIR | 0755);
+	close(fd);
+	snprint(o, sizeof(o), ".git/objects/%c%c/%s", s[0], s[1], s + 2);
+	if(readobject(*h) == nil){
+		if((f = Bopen(o, OWRITE)) == nil)
+			sysfatal("could not open %s: %r", o);
+		if(deflatezlib(f, bwrite, &b, objbytes, 9, 0) == -1)
+			sysfatal("could not write %s: %r", o);
+		Bterm(f);
+	}
+}
+
+int
+gitmode(int m)
+{
+	return (m & 0777) | ((m & DMDIR) ? 0040000 : 0100000);
+}
+
+void
+blobify(char *path, vlong size, Hash *bh)
+{
+	char h[64], *d;
+	int f, nh;
+
+	nh = snprint(h, sizeof(h), "%T %lld", GBlob, size) + 1;
+	if((f = open(path, OREAD)) == -1)
+		sysfatal("could not open %s: %r", path);
+	d = emalloc(size);
+	if(readn(f, d, size) != size)
+		sysfatal("could not read blob %s: %r", path);
+	writeobj(bh, h, nh, d, size);
+	close(f);
+	free(d);
+}
+
+int
+tracked(char *path)
+{
+	Dir *d;
+	char ipath[256];
+
+	/* Explicitly removed. */
+	snprint(ipath, sizeof(ipath), ".git/index9/removed/%s", path);
+	if(strstr(cleanname(ipath), ".git/index9/removed") != ipath)
+		sysfatal("path %s leaves index", ipath);
+	d = dirstat(ipath);
+	if(d != nil && d->qid.type != QTDIR){
+		free(d);
+		return 0;
+	}
+
+	/* Explicitly added. */
+	snprint(ipath, sizeof(ipath), ".git/index9/tracked/%s", path);
+	if(strstr(cleanname(ipath), ".git/index9/tracked") != ipath)
+		sysfatal("path %s leaves index", ipath);
+	if(access(ipath, AEXIST) == 0)
+		return 1;
+
+	return 0;
+}
+
+int
+dircmp(void *pa, void *pb)
+{
+	char aname[256], bname[256], c;
+	Dir *a, *b;
+
+	a = pa;
+	b = pb;
+	/*
+	 * If the files have the same name, they're equal.
+	 * Otherwise, If they're trees, they sort as thoug
+	 * there was a trailing slash.
+	 *
+	 * Wat.
+	 */
+	if(strcmp(a->name, b->name) == 0){
+		snprint(aname, sizeof(aname), "%s", a->name);
+		snprint(bname, sizeof(bname), "%s", b->name);
+	}else{
+		c = (a->qid.type & QTDIR) ? '/' : 0;
+		snprint(aname, sizeof(aname), "%s%c", a->name, c);
+		c = (b->qid.type & QTDIR) ? '/' : 0;
+		snprint(bname, sizeof(bname), "%s%c", b->name, c);
+	}
+
+	return strcmp(aname, bname);
+}
+
+int
+treeify(char *path, Hash *th)
+{
+	char *t, h[64], l[256], ep[256];
+	int nd, nl, nt, nh, i, s;
+	Hash eh;
+	Dir *d;
+		
+	if((nd = slurpdir(path, &d)) == -1)
+		sysfatal("could not read %s", path);
+	if(nd == 0)
+		return 0;
+
+	t = nil;
+	nt = 0;
+	qsort(d, nd, sizeof(Dir), dircmp);
+	for(i = 0; i < nd; i++){
+		snprint(ep, sizeof(ep), "%s/%s", path, d[i].name);
+		if(strcmp(d[i].name, ".git") == 0)
+			continue;
+		if(!tracked(ep))
+			continue;
+		if((d[i].qid.type & QTDIR) == 0)
+			blobify(ep, d[i].length, &eh);
+		else if(treeify(ep, &eh) == 0)
+			continue;
+
+		nl = snprint(l, sizeof(l), "%o %s", gitmode(d[i].mode), d[i].name);
+		s = nt + nl + sizeof(eh.h) + 1;
+		t = realloc(t, s);
+		memcpy(t + nt, l, nl + 1);
+		memcpy(t + nt + nl + 1, eh.h, sizeof(eh.h));
+		nt = s;
+	}
+	free(d);
+	nh = snprint(h, sizeof(h), "%T %d", GTree, nt) + 1;
+	if(nh >= sizeof(h))
+		sysfatal("overlong header");
+	writeobj(th, h, nh, t, nt);
+	free(t);
+	return nd;
+}
+
+
+void
+mkcommit(Hash *c, char *msg, char *name, char *email, Hash *parents, int nparents, Hash tree)
+{
+	char *s, h[64];
+	int ns, nh, i;
+	Fmt f;
+
+	fmtstrinit(&f);
+	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, (vlong)time(nil));
+	fmtprint(&f, "committer %s <%s> %lld +0000\n", name, email, (vlong)time(nil));
+	fmtprint(&f, "\n");
+	fmtprint(&f, "%s", msg);
+	s = fmtstrflush(&f);
+
+	ns = strlen(s);
+	nh = snprint(h, sizeof(h), "%T %d", GCommit, ns) + 1;
+	writeobj(c, h, nh, s, ns);
+	free(s);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: git/commit -n name -e email -m message -d dir");
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	Hash c, t, parents[Maxparents];
+	char *msg, *name, *email;
+	int r, nparents;
+
+
+	msg = nil;
+	name = nil;
+	email = nil;
+	nparents = 0;
+	gitinit();
+	ARGBEGIN{
+	case 'm':	msg = EARGF(usage());	break;
+	case 'n':	name = EARGF(usage());	break;
+	case 'e':	email = EARGF(usage());	break;
+	case 'p':
+		if(nparents >= Maxparents)
+			sysfatal("too many parents");
+		if(resolveref(&parents[nparents++], EARGF(usage())) == -1)
+			sysfatal("invalid parent: %r");
+		break;
+	}ARGEND;
+
+	if(!msg) sysfatal("missing message");
+	if(!name) sysfatal("missing name");
+	if(!email) sysfatal("missing email");
+	if(!msg || !name)
+		usage();
+
+	gitinit();
+	if(access(".git", AEXIST) != 0)
+		sysfatal("could not find git repo: %r");
+	r = treeify(".", &t);
+	if(r == -1)
+		sysfatal("could not commit: %r\n");
+	if(r == 0)
+		sysfatal("empty commit: aborting");
+	mkcommit(&c, msg, name, email, parents, nparents, t);
+	print("%H\n", c);
+	exits(nil);
+}
--- /dev/null
+++ b/send.c
@@ -1,0 +1,425 @@
+#include <u.h>
+#include <libc.h>
+#include <pool.h>
+
+#include "git.h"
+
+typedef struct Objq	Objq;
+typedef struct Buf	Buf;
+typedef struct Compout	Compout;
+typedef struct Update	Update;
+
+struct Buf {
+	int off;
+	int sz;
+	uchar *data;
+};
+
+struct Compout {
+	int fd;
+	DigestState *st;
+};
+
+struct Objq {
+	Objq *next;
+	Object *obj;
+};
+
+struct Update {
+	char	ref[128];
+	Hash	theirs;
+	Hash	ours;
+};
+
+int sendall;
+char *curbranch = "refs/heads/master";
+char *removed[128];
+int nremoved;
+
+static int
+hwrite(int fd, void *buf, int nbuf, DigestState **st)
+{
+	if(write(fd, buf, nbuf) != nbuf)
+		return -1;
+	*st = sha1(buf, nbuf, nil, *st);
+	return nbuf;
+}
+
+void
+pack(Objset *send, Objset *skip, Object *o)
+{
+	Dirent *e;
+	Object *s;
+	int i;
+
+	if(oshas(send, o) || oshas(skip, o))
+		return;
+	osadd(send, o);
+	switch(o->type){
+	case GCommit:
+		if((s = readobject(o->commit->tree)) == nil)
+			sysfatal("could not read tree %H: %r", o->hash);
+		pack(send, skip, s);
+		break;
+	case GTree:
+		for(i = 0; i < o->tree->nent; i++){
+			e = &o->tree->ent[i];
+			if(e->modref)
+				print("wtf, a link? %s\n", e->name);
+			if ((s = readobject(e->h)) == nil)
+				sysfatal("could not read entry %H: %r", e->h);
+			pack(send, skip, s);
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+int
+compread(void *p, void *dst, int n)
+{
+	Buf *b;
+
+	b = p;
+	if(n > b->sz - b->off)
+		n = b->sz - b->off;
+	memcpy(dst, b->data + b->off, n);
+	b->off += n;
+	return n;
+}
+
+int
+compwrite(void *p, void *buf, int n)
+{
+	Compout *o;
+
+	o = p;
+	o->st = sha1(buf, n, nil, o->st);
+	return write(o->fd, buf, n);
+}
+
+int
+compress(int fd, void *buf, int sz, DigestState **st)
+{
+	int r;
+	Buf b ={
+		.off=0,
+		.data=buf,
+		.sz=sz,
+	};
+	Compout o = {
+		.fd = fd,
+		.st = *st,
+	};
+
+	r = deflatezlib(&o, compwrite, &b, compread, 6, 0);
+	*st = o.st;
+	return r;
+}
+
+int
+writeobject(int fd, Object *o, DigestState **st)
+{
+	char hdr[8];
+	uvlong sz;
+	int i;
+
+	i = 1;
+	sz = o->size;
+	hdr[0] = o->type << 4;
+	hdr[0] |= sz & 0xf;
+	if(sz >= (1 << 4)){
+		hdr[0] |= 0x80;
+		sz >>= 4;
+	
+		for(i = 1; i < sizeof(hdr); i++){
+			hdr[i] = sz & 0x7f;
+			if(sz <= 0x7f){
+				i++;
+				break;
+			}
+			hdr[i] |= 0x80;
+			sz >>= 7;
+		}
+	}
+
+	if(hwrite(fd, hdr, i, st) != i)
+		return -1;
+	if(compress(fd, o->data, o->size, st) == -1)
+		return -1;
+	return 0;
+}
+
+int
+writepack(int fd, Update *upd, int nupd)
+{
+	Objset send, skip;
+	Object *o, *p;
+	Objq *q, *n, *e;
+	DigestState *st;
+	Update *u;
+	char buf[4];
+	Hash h;
+	int i;
+
+	osinit(&send);
+	osinit(&skip);
+	for(i = 0; i < nupd; i++){
+		u = &upd[i];
+		if(hasheq(&u->theirs, &Zhash))
+			continue;
+		if((o = readobject(u->theirs)) == nil)
+			sysfatal("could not read %H", u->theirs);
+		pack(&skip, &skip, o);
+	}
+
+	q = nil;
+	e = nil;
+	for(i = 0; i < nupd; i++){
+		u = &upd[i];
+		if((o = readobject(u->ours)) == nil)
+			sysfatal("could not read object %H", u->ours);
+
+		n = emalloc(sizeof(Objq));
+		n->obj = o;
+		if(!q){
+			q = n;
+			e = n;
+		}else{
+			e->next = n;
+		}
+	}
+
+	for(n = q; n; n = n->next)
+		e = n;
+	for(; q; q = n){
+		o = q->obj;
+		if(oshas(&skip, o) || oshas(&send, o))
+			goto iter;
+		pack(&send, &skip, o);
+		for(i = 0; i < o->commit->nparent; i++){
+			if((p = readobject(o->commit->parent[i])) == nil)
+				sysfatal("could not read parent of %H", o->hash);
+			e->next = emalloc(sizeof(Objq));
+			e->next->obj = p;
+			e = e->next;
+		}
+iter:
+		n = q->next;
+		free(q);
+	}
+
+	st = nil;
+	PUTBE32(buf, send.nobj);
+	if(hwrite(fd, "PACK\0\0\0\02", 8, &st) != 8)
+		return -1;
+	if(hwrite(fd, buf, 4, &st) == -1)
+		return -1;
+	for(i = 0; i < send.sz; i++){
+		if(!send.obj[i])
+			continue;
+		if(writeobject(fd, send.obj[i], &st) == -1)
+			return -1;
+	}
+	sha1(nil, 0, h.h, st);
+	if(write(fd, h.h, sizeof(h.h)) == -1)
+		return -1;
+	return 0;
+}
+
+Update*
+findref(Update *u, int nu, char *ref)
+{
+	int i;
+
+	for(i = 0; i < nu; i++)
+		if(strcmp(u[i].ref, ref) == 0)
+			return &u[i];
+	return nil;
+}
+
+int
+readours(Update **ret)
+{
+	Update *u, *r;
+	Hash *h;
+	int nd, nu, i;
+	char *pfx;
+	Dir *d;
+
+	nu = 0;
+	if(!sendall){
+		u = emalloc((nremoved + 1)*sizeof(Update));
+		snprint(u[nu].ref, sizeof(u[nu].ref), "%s", curbranch);
+		if(resolveref(&u[nu].ours, curbranch) == -1)
+			sysfatal("broken branch %s", curbranch);
+		nu++;
+	}else{
+		if((nd = slurpdir(".git/refs/heads", &d)) == -1)
+			sysfatal("read branches: %r");
+		u = emalloc((nremoved + nd)*sizeof(Update));
+		for(i = 0; i < nd; i++){
+			snprint(u->ref, sizeof(u->ref), "refs/heads/%s", d[nu].name);
+			if(resolveref(&u[nu].ours, u[nu].ref) == -1)
+				continue;
+			nu++;
+		}
+	}
+	for(i = 0; i < nremoved; i++){
+		pfx = "refs/heads/";
+		if(strstr(removed[i], "heads/") == removed[i])
+			pfx = "refs/";
+		if(strstr(removed[i], "refs/heads/") == removed[i])
+			pfx = "";
+		snprint(u[nu].ref, sizeof(u[nu].ref), "%s%s", pfx, removed[i]);
+		h = &u[nu].ours;
+		if((r = findref(u, nu, u[nu].ref)) != nil)
+			h = &r->ours;
+		else
+			nu++;
+		memcpy(h, &Zhash, sizeof(Hash));
+	}
+
+	*ret = u;
+	return nu;	
+}
+
+int
+sendpack(int fd)
+{
+	char buf[65536];
+	char *sp[3], *p;
+	Update *upd, *u;
+	int i, n, nupd, nsp, updating;
+
+	if((nupd = readours(&upd)) == -1)
+		sysfatal("read refs: %r");
+	while(1){
+		n = readpkt(fd, buf, sizeof(buf));
+		if(n == -1)
+			return -1;
+		if(n == 0)
+			break;
+		if(strncmp(buf, "ERR ", 4) == 0)
+			sysfatal("%s", buf + 4);
+
+		if(getfields(buf, sp, nelem(sp), 1, " \t\n\r") != 2)
+			sysfatal("invalid ref line %.*s", utfnlen(buf, n), buf);
+		if((u = findref(upd, nupd, sp[1])) == nil)
+			continue;
+		if(hparse(&u->theirs, sp[0]) == -1)
+			sysfatal("invalid hash %s", sp[0]);
+		snprint(u->ref, sizeof(u->ref), sp[1]);
+	}
+
+	updating = 0;
+	for(i = 0; i < nupd; i++){
+		u = &upd[i];
+		if(!hasheq(&u->theirs, &Zhash) && readobject(u->theirs) == nil){
+			fprint(2, "remote has diverged: pull and try again\n");
+			updating = 0;
+			break;
+		}
+		if(hasheq(&u->ours, &Zhash)){
+			print("%s: deleting\n", u->ref);
+			continue;
+		}
+		if(hasheq(&u->theirs, &u->ours)){
+			print("%s: up to date\n", u->ref);
+			continue;
+		}
+		print("%s: %H => %H\n", u->ref, u->theirs, u->ours);
+		n = snprint(buf, sizeof(buf), "%H %H %s", u->theirs, u->ours, u->ref);
+
+		/*
+		 * Workaround for github.
+		 *
+		 * Github will accept the pack but fail to update the references
+		 * if we don't have capabilities advertised. Report-status seems
+		 * harmless to add, so we add it.
+		 *
+		 * Github doesn't advertise any capabilities, so we can't check
+		 * for compatibility. We just need to add it blindly.
+		 */
+		if(i == 0){
+			buf[n++] = '\0';
+			n += snprint(buf + n, sizeof(buf) - n, " report-status");
+		}
+		if(writepkt(fd, buf, n) == -1)
+			sysfatal("unable to send update pkt");
+		updating = 1;
+	}
+	flushpkt(fd);
+	if(updating){
+		if(writepack(fd, upd, nupd) == -1)
+			return -1;
+
+		/* We asked for a status report, may as well use it. */
+		while((n = readpkt(fd, buf, sizeof(buf))) > 0){
+ 			buf[n] = 0;
+			nsp = getfields(buf, sp, nelem(sp), 1, " \t\n\r");
+			if(nsp < 2) 
+				continue;
+			if(nsp < 3)
+				sp[2] = "";
+			if(strcmp(sp[0], "unpack") == 0 && strcmp(sp[1], "ok") != 0)
+				fprint(2, "unpack %s\n", sp[1]);
+			else if(strcmp(sp[0], "ok") == 0)
+				fprint(2, "%s: updated\n", sp[1]);
+			else if(strcmp(sp[0], "ng") == 0)
+				fprint(2, "failed update: %s\n", sp[1]);
+		}
+	}
+	return 0;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s remote [reponame]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char proto[Nproto], host[Nhost], port[Nport];
+	char repo[Nrepo], path[Npath];
+	int fd;
+
+	ARGBEGIN{
+	default:	usage();	break;
+	case 'a':	sendall++;	break;
+	case 'd':	chattygit++;	break;
+	case 'r':
+		if(nremoved == nelem(removed))
+			sysfatal("too many deleted branches");
+		removed[nremoved++] = EARGF(usage());
+		break;
+	case 'b':
+		curbranch = smprint("refs/%s", EARGF(usage()));
+		break;
+	}ARGEND;
+
+	gitinit();
+	if(argc != 1)
+		usage();
+	fd = -1;
+	if(parseuri(argv[0], proto, host, port, path, repo) == -1)
+		sysfatal("bad uri %s", argv[0]);
+	if(strcmp(proto, "ssh") == 0 || strcmp(proto, "git+ssh") == 0)
+		fd = dialssh(host, port, path, "receive");
+	else if(strcmp(proto, "git") == 0)
+		fd = dialgit(host, port, path, "receive");
+	else if(strcmp(proto, "http") == 0 || strcmp(proto, "git+http") == 0)
+		sysfatal("http clone not implemented");
+	else
+		sysfatal("unknown protocol %s", proto);
+	
+	if(fd == -1)
+		sysfatal("could not dial %s:%s: %r", proto, host);
+	if(sendpack(fd) == -1)
+		sysfatal("send failed: %r");
+	exits(nil);
+}
--- /dev/null
+++ b/template/HEAD
@@ -1,0 +1,1 @@
+ref: refs/heads/master
--- /dev/null
+++ b/template/config
@@ -1,0 +1,5 @@
+[core]
+	repositoryformatversion = 0
+	filemode = true
+	bare = false
+	logallrefupdates = true
--- /dev/null
+++ b/template/description
@@ -1,0 +1,1 @@
+Unnamed repository; edit this file 'description' to name the repository.
--- /dev/null
+++ b/template/info/exclude
@@ -1,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
--- /dev/null
+++ b/util.c
@@ -1,0 +1,235 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+Reprog *authorpat;
+Hash Zhash;
+
+int
+hasheq(Hash *a, Hash *b)
+{
+	return memcmp(a->h, b->h, sizeof(a->h)) == 0;
+}
+
+static int
+charval(int c, int *err)
+{
+	if(c >= '0' && c <= '9')
+		return c - '0';
+	if(c >= 'a' && c <= 'f')
+		return c - 'a' + 10;
+	if(c >= 'A' && c <= 'F')
+		return c - 'A' + 10;
+	*err = 1;
+	return -1;
+}
+
+void *
+emalloc(ulong n)
+{
+	void *v;
+	
+	v = mallocz(n, 1);
+	if(v == nil)
+		sysfatal("malloc: %r");
+	setmalloctag(v, getcallerpc(&n));
+	return v;
+}
+
+void *
+erealloc(void *p, ulong n)
+{
+	void *v;
+	
+	v = realloc(p, n);
+	if(v == nil)
+		sysfatal("realloc: %r");
+	setmalloctag(v, getcallerpc(&n));
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	s = strdup(s);
+	if(s == nil)
+		sysfatal("strdup: %r");
+	setmalloctag(s, getcallerpc(&s));
+	return s;
+}
+
+int
+Hfmt(Fmt *fmt)
+{
+	Hash h;
+	int i, n, l;
+	char c0, c1;
+
+	l = 0;
+	h = va_arg(fmt->args, Hash);
+	for(i = 0; i < sizeof h.h; i++){
+		n = (h.h[i] >> 4) & 0xf;
+		c0 = (n >= 10) ? n-10 + 'a' : n + '0';
+		n = h.h[i] & 0xf;
+		c1 = (n >= 10) ? n-10 + 'a' : n + '0';
+		l += fmtprint(fmt, "%c%c", c0, c1);
+	}
+	return l;
+}
+
+int
+Tfmt(Fmt *fmt)
+{
+	Type t;
+	int l;
+
+	t = va_arg(fmt->args, Type);
+	switch(t){
+	case GNone:	l = fmtprint(fmt, "none");	break;
+	case GCommit:	l = fmtprint(fmt, "commit");	break;
+	case GTree:	l = fmtprint(fmt, "tree");	break;
+	case GBlob:	l = fmtprint(fmt, "blob");	break;
+	case GTag:	l = fmtprint(fmt, "tag");	break;
+	case GOdelta:	l = fmtprint(fmt, "odelta");	break;
+	case GRdelta:	l = fmtprint(fmt, "gdelta");	break;
+	default:	l = fmtprint(fmt, "?%d?", t);	break;
+	}
+	return l;
+}
+
+int
+Ofmt(Fmt *fmt)
+{
+	Object *o;
+	int l;
+
+	o = va_arg(fmt->args, Object *);
+	print("== %H (%T) ==\n", o->hash, o->type);
+	switch(o->type){
+	case GTree:
+		l = fmtprint(fmt, "tree\n");
+		break;
+	case GBlob:
+		l = fmtprint(fmt, "blob %s\n", o->data);
+		break;
+	case GCommit:
+		l = fmtprint(fmt, "commit\n");
+		break;
+	case GTag:
+		l = fmtprint(fmt, "tag\n");
+		break;
+	default:
+		l = fmtprint(fmt, "invalid: %d\n", o->type);
+		break;
+	}
+	return l;
+}
+
+int
+Qfmt(Fmt *fmt)
+{
+	Qid q;
+
+	q = va_arg(fmt->args, Qid);
+	return fmtprint(fmt, "Qid{path=0x%llx(dir:%d,obj:%lld), vers=%ld, type=%d}",
+	    q.path, QDIR(&q), (q.path >> 8), q.vers, q.type);
+}
+
+void
+gitinit(void)
+{
+	fmtinstall('H', Hfmt);
+	fmtinstall('T', Tfmt);
+	fmtinstall('O', Ofmt);
+	fmtinstall('Q', Qfmt);
+	inflateinit();
+	deflateinit();
+	authorpat = regcomp("[\t ]*(.*)[\t ]+([0-9]+)[\t ]+([\\-+]?[0-9]+)");
+	osinit(&objcache);
+}
+
+int
+hparse(Hash *h, char *b)
+{
+	int i, err;
+
+	err = 0;
+	for(i = 0; i < sizeof(h->h); i++){
+		err = 0;
+		h->h[i] = 0;
+		h->h[i] |= ((charval(b[2*i], &err) & 0xf) << 4);
+		h->h[i] |= ((charval(b[2*i+1], &err)& 0xf) << 0);
+		if(err){
+			werrstr("invalid hash");
+			return -1;
+		}
+	}
+	return 0;
+}
+
+int
+slurpdir(char *p, Dir **d)
+{
+	int r, f;
+
+	if((f = open(p, OREAD)) == -1)
+		return -1;
+	r = dirreadall(f, d);
+	close(f);
+	return r;
+}	
+
+int
+hassuffix(char *base, char *suf)
+{
+	int nb, ns;
+
+	nb = strlen(base);
+	ns = strlen(suf);
+	if(ns <= nb && strcmp(base + (nb - ns), suf) == 0)
+		return 1;
+	return 0;
+}
+
+int
+swapsuffix(char *dst, int dstsz, char *base, char *oldsuf, char *suf)
+{
+	int bl, ol, sl, l;
+
+	bl = strlen(base);
+	ol = strlen(oldsuf);
+	sl = strlen(suf);
+	l = bl + sl - ol;
+	if(l + 1 > dstsz || ol > bl)
+		return -1;
+	memmove(dst, base, bl - ol);
+	memmove(dst + bl - ol, suf, sl);
+	dst[l] = 0;
+	return l;
+}
+
+char *
+strip(char *s)
+{
+	char *e;
+
+	while(isspace(*s))
+		s++;
+	e = s + strlen(s);
+	while(e > s && isspace(*--e))
+		*e = 0;
+	return s;
+}
+
+void
+die(char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	vfprint(2, fmt, ap);
+	va_end(ap);
+	abort();
+}
--- /dev/null
+++ b/walk.c
@@ -1,0 +1,295 @@
+#include <u.h>
+#include <libc.h>
+#include "git.h"
+
+#define NCACHE 256
+#define TDIR ".git/index9/tracked"
+#define RDIR ".git/index9/removed"
+#define HDIR "/mnt/git/HEAD/tree"
+typedef struct Cache	Cache;
+typedef struct Wres	Wres;
+struct Cache {
+	Dir*	cache;
+	int	n;
+	int	max;
+};
+
+struct Wres {
+	char	**path;
+	int	npath;
+	int	pathsz;
+};
+
+enum {
+	Rflg	= 1 << 0,
+	Mflg	= 1 << 1,
+	Aflg	= 1 << 2,
+	Tflg	= 1 << 3,
+};
+
+Cache seencache[NCACHE];
+int quiet;
+int printflg;
+char *rstr = "R ";
+char *tstr = "T ";
+char *mstr = "M ";
+char *astr = "A ";
+
+int
+seen(Dir *dir)
+{
+	Dir *dp;
+	int i;
+	Cache *c;
+
+	c = &seencache[dir->qid.path&(NCACHE-1)];
+	dp = c->cache;
+	for(i=0; i<c->n; i++, dp++)
+		if(dir->qid.path == dp->qid.path &&
+		   dir->type == dp->type &&
+		   dir->dev == dp->dev)
+			return 1;
+	if(c->n == c->max){
+		if (c->max == 0)
+			c->max = 8;
+		else
+			c->max += c->max/2;
+		c->cache = realloc(c->cache, c->max*sizeof(Dir));
+		if(c->cache == nil)
+			sysfatal("realloc: %r");
+	}
+	c->cache[c->n++] = *dir;
+	return 0;
+}
+
+int
+readpaths(Wres *r, char *pfx, char *dir)
+{
+	char *f, *sub, *full, *sep;
+	Dir *d;
+	int fd, ret, i, n;
+
+	ret = -1;
+	sep = "";
+	if(dir[0] != 0)
+		sep = "/";
+	if((full = smprint("%s/%s", pfx, dir)) == nil)
+		sysfatal("smprint: %r");
+	if((fd = open(full, OREAD)) < 0)
+		goto error;
+	while((n = dirread(fd, &d)) > 0){
+		for(i = 0; i < n; i++){
+			if(seen(&d[i]))
+				continue;
+			if(d[i].qid.type & QTDIR){
+				if((sub = smprint("%s%s%s", dir, sep, d[i].name)) == nil)
+					sysfatal("smprint: %r");
+				if(readpaths(r, pfx, sub) == -1){
+					free(sub);
+					goto error;
+				}
+				free(sub);
+			}else{
+				if(r->npath == r->pathsz){
+					r->pathsz = 2*r->pathsz + 1;
+					r->path = erealloc(r->path, r->pathsz * sizeof(char*));
+				}
+				if((f = smprint("%s%s%s", dir, sep, d[i].name)) == nil)
+					sysfatal("smprint: %r");
+				r->path[r->npath++] = f;
+			}
+		}
+	}
+	ret = r->npath;
+error:
+	close(fd);
+	free(full);
+	free(d);
+	return ret;
+}
+
+int
+cmp(void *pa, void *pb)
+{
+	return strcmp(*(char **)pa, *(char **)pb);
+}
+
+void
+dedup(Wres *r)
+{
+	int i, o;
+
+	if(r->npath <= 1)
+		return;
+	o = 0;
+	qsort(r->path, r->npath, sizeof(r->path[0]), cmp);
+	for(i = 1; i < r->npath; i++)
+		if(strcmp(r->path[o], r->path[i]) != 0)
+			r->path[++o] = r->path[i];
+	r->npath = o + 1;
+}
+
+static void
+findroot(void)
+{
+	char path[256], buf[256], *p;
+
+	if(access("/mnt/git/ctl", AEXIST) != 0)
+		sysfatal("no running git/fs");
+	if((getwd(path, sizeof(path))) == nil)
+		sysfatal("could not get wd: %r");
+	while((p = strrchr(path, '/')) != nil){
+		snprint(buf, sizeof(buf), "%s/.git", path);
+		if(access(buf, AEXIST) == 0){
+			chdir(path);
+			return;
+		}
+		*p = '\0';
+	}
+	sysfatal("not a git repository");
+}
+
+int
+sameqid(char *f, char *qf)
+{
+	char indexqid[64], fileqid[64], *p;
+	Dir *d;
+	int fd, n;
+
+	if((fd = open(qf, OREAD)) == -1)
+		return -1;
+	if((n = readn(fd, indexqid, sizeof(indexqid) - 1)) == -1)
+		return -1;
+	indexqid[n] = 0;
+	close(fd);
+	if((p = strpbrk(indexqid, "  \t\n\r")) != nil)
+		*p = 0;
+
+	if((d = dirstat(f)) == nil)
+		return -1;
+	snprint(fileqid, sizeof(fileqid), "%ullx.%uld.%.2uhhx",
+	    d->qid.path, d->qid.vers, d->qid.type);
+	if(strcmp(indexqid, fileqid) == 0)
+		return 1;
+	return 0;
+}
+
+int
+samedata(char *pa, char *pb)
+{
+	char ba[32*1024], bb[32*1024];
+	int fa, fb, na, nb, same;
+
+	same = 0;
+	fa = open(pa, OREAD);
+	fb = open(pb, OREAD);
+	if(fa == -1 || fb == -1){
+		goto mismatch;
+	}
+	while(1){
+		if((na = readn(fa, ba, sizeof(ba))) == -1)
+			goto mismatch;
+		if((nb = readn(fb, bb, sizeof(bb))) == -1)
+			goto mismatch;
+		if(na != nb)
+			goto mismatch;
+		if(na == 0)
+			break;
+		if(memcmp(ba, bb, na) != 0)
+			goto mismatch;
+	}
+	same = 1;
+mismatch:
+	if(fa != -1)
+		close(fa);
+	if(fb != -1)
+		close(fb);
+	return same;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-qbc] [-f filt]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char rmpath[256], tpath[256], bpath[256], buf[8];
+	char *p, *e;
+	int i, dirty;
+	Wres r;
+
+	ARGBEGIN{
+	case 'q':
+		quiet++;
+		break;
+	case 'c':
+		rstr = "";
+		tstr = "";
+		mstr = "";
+		astr = "";
+		break;
+	case 'f':
+		for(p = EARGF(usage()); *p; p++)
+			switch(*p){
+			case 'T':	printflg |= Tflg;	break;
+			case 'A':	printflg |= Aflg;	break;
+			case 'M':	printflg |= Mflg;	break;
+			case 'R':	printflg |= Rflg;	break;
+			default:	usage();		break;
+		}
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	findroot();
+	dirty = 0;
+	r.path = nil;
+	r.npath = 0;
+	r.pathsz = 0;
+	if(access("/mnt/git/ctl", AEXIST) != 0)
+		sysfatal("git/fs does not seem to be running");
+	if(printflg == 0)
+		printflg = Tflg | Aflg | Mflg | Rflg;
+	if(access(TDIR, AEXIST) == 0 && readpaths(&r, TDIR, "") == -1)
+		sysfatal("read tracked: %r");
+	if(access(RDIR, AEXIST) == 0 && readpaths(&r, RDIR, "") == -1)
+		sysfatal("read removed: %r");
+	dedup(&r);
+
+	for(i = 0; i < r.npath; i++){
+		p = r.path[i];
+		snprint(rmpath, sizeof(rmpath), RDIR"/%s", p);
+		snprint(tpath, sizeof(tpath), TDIR"/%s", p);
+		snprint(bpath, sizeof(bpath), HDIR"/%s", p);
+		if(access(p, AEXIST) != 0 || access(rmpath, AEXIST) == 0){
+			dirty |= Mflg;
+			if(!quiet && (printflg & Rflg))
+				print("%s%s\n", rstr, p);
+		}else if(access(bpath, AEXIST) == -1) {
+			dirty |= Aflg;
+			if(!quiet && (printflg & Aflg))
+				print("%s%s\n", astr, p);
+		}else if(!sameqid(p, tpath) && !samedata(p, bpath)){
+			dirty |= Mflg;
+			if(!quiet && (printflg & Mflg))
+				print("%s%s\n", mstr, p);
+		}else{
+			if(!quiet && (printflg & Tflg))
+				print("%s%s\n", tstr, p);
+		}
+	}
+	if(!dirty)
+		exits(nil);
+
+	p = buf;
+	e = buf + sizeof(buf);
+	for(i = 0; (1 << i) != Tflg; i++)
+		if(dirty & (1 << i))
+			p = seprint(p, e, "%c", "DMAT"[i]);
+	exits(buf);
+}