code: plan9front

Download patch

ref: 1ee1bfaa8c5644092e0c1ca3985ee74813bbfb1d
parent: 013b2cad191eef50fd4e69c38f1544c5083b640d
author: Ori Bernstein <ori@eigenstate.org>
date: Sun May 16 14:49:45 EDT 2021

git: got git?

Add a snapshot of git9 to 9front.

diff: cannot open b/sys/src/cmd/git//null: file does not exist: 'b/sys/src/cmd/git//null'
--- /dev/null
+++ b/sys/man/1/git
@@ -1,0 +1,643 @@
+.TH GIT 1
+.SH NAME
+git, git/conf, git/query, git/walk, git/clone, git/branch,
+git/commit, git/diff, git/init, git/log, git/merge, git/push,
+git/pull, git/rm, git/serve
+\- Manage git repositories.
+
+.SH SYNOPSIS
+.PP
+.B git/add
+[
+.B -r
+]
+.I path...
+.PP
+.B git/rm
+.I path...
+.PP
+.B git/branch
+[
+.B -adns
+]
+[
+.B -b
+.I base
+]
+.I newbranch
+.PP
+.B git/clone
+[
+.I remote
+[
+.I local
+]
+]
+.PP
+.B git/commit
+[
+.B -re
+]
+[
+.B -m msg
+]
+[
+.I file...
+]
+.PP
+.B git/compat
+.PP
+.B git/conf
+[
+.B -r
+]
+[
+.B -f
+.I file
+]
+.I keys...
+.PP
+.B git/diff
+[
+.B -c
+.I branch
+]
+[
+.B -s
+]
+[
+.I file...
+]
+.PP
+.B git/revert
+[
+.B -c
+.I commit
+]
+.I file...
+.PP
+.B git/export
+[
+.I commits...
+]
+.PP
+.B git/import
+[
+.I commits...
+]
+.PP
+.B git/init
+[
+.B -b
+]
+[
+.I dir
+]
+[
+.B -u
+.I upstream
+]
+.PP
+.B git/log
+[
+.B -c
+.I commit
+.B | -e
+.I expr
+]
+[
+.B -s
+]
+[
+.I files...
+]
+.PP
+.B git/merge
+.I theirs
+.PP
+.B git/rebase
+[
+.B -ari
+]
+[
+.B onto
+]
+.PP
+.B git/pull
+[
+.B -f
+]
+[
+.B -q
+]
+[
+.B -a
+]
+[
+.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/serve
+[
+.B -w
+]
+[
+.B -r
+.I path
+]
+.PP
+.B git/query
+[
+.B -pcr
+]
+.I query
+.PP
+.B git/walk
+[
+.B -qc
+]
+[
+.B -b
+.I branch
+]
+[
+.B -f
+.I filters
+]
+[
+.I [files...]
+]
+
+.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.
+
+.PP
+.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.
+
+.PP
+.B Git/revert
+restores the named files from HEAD. When passed the -c flag, restores files from
+the named commit.
+
+.PP
+.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.
+
+.PP
+.B Git/serve
+serves repositories using the
+.I git://
+protocol over stdin.
+By default, it serves them read-only.
+The 
+.I -w
+flag, it allows pushing into repositories.
+The
+.I -r
+.B path
+flag serves repositories relative to
+.BR path .
+
+.PP
+.B Git/fs 
+serves a file system on /mnt/git.
+For full documentation, see
+.IR gitfs (4)
+
+.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/rm
+is an alias for
+.IR git/add -r .
+
+.PP
+.B Git/commit
+creates a new commit consisting of all changes to the specified files.
+By default, an editor is opened to prepare the commit message.
+The
+.I -m
+flag supplies the commit message directly.
+The
+.I -r
+flag revises the contents of the previous commit, reusing the message.
+The
+.I -e
+flag opens an editor to finalize the commit message, regardless of
+whether or not it was specified explicitly or reused.
+To amend a commit message,
+.I -r
+can be used in conjuction with
+.I -m
+or
+.IR -e .
+
+.PP
+.B Git/branch
+is used to list or switch branches.
+When invoked with no arguments, it lists the current branch.
+To list all branches, pass the
+.I -a
+option.
+To switch between branches, pass a branch name.
+When passed the
+.I -n
+option, the branch will be created, overwriting existing branch.
+When passed the
+.I -b base
+option, the branch created is based off of
+.I base
+instead of
+.I HEAD.
+When passed the
+.I -s
+option, the branch is created but the files are not checked out.
+When passed the
+.I -d
+option, the branch is deleted.
+
+.PP
+.B Git/log
+shows a history of the current branch.
+When passed a list of files, only commits affecting
+those files are shown.
+The
+.I -c commit
+option logs starting from the provided commit, instead of HEAD.
+The
+.I -s
+option shows a summary of the commit, instead of the full message.
+The
+.I -e expr
+option shows commits matching the query expression provided.
+The expression is in the syntax of
+.B git/query.
+
+.PP
+.B Git/diff
+shows the differences between the currently checked out code and
+the
+.I HEAD
+commit.
+When passed the
+.I -c base
+option, the diff is computed against
+.I base
+instead of
+.I HEAD.
+When passed the
+.I -s
+option, only the file statuses are
+printed.
+
+.PP
+.B Git/export
+exports a list of commits in a format that
+.B git/import
+can apply.
+
+.PP
+.B Git/import
+imports a commit with message, author, and
+date information.
+
+.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/rebase
+takes one branch and moves it onto another.
+On error, the remaining commits to rebase are
+saved, and can be resumed once the conflict is
+resolved using the
+.I -r
+option.
+If the rebase is to be aborted, the
+.I -a
+option will clean up the in progress rebase
+and reset the state of the branch.
+The
+.I -i
+option will open an editor to modify the todo-list before the rebase
+begins.
+
+.PP
+The following rebase commands are supported:
+.TP 10
+.B pick
+Apply the commit.
+.TP
+.B reword
+Apply the commit, then edit its commit message.
+.TP
+.B edit
+Apply the commit, then exit to allow further changes.
+.TP
+.B squash
+Fold the commit into the previous commit, then edit the combined
+commit message.
+.TP
+.B fixup
+Fold the commit into the previous commit, discarding its commit
+message.
+.TP
+.B break
+Exit to allow for manual edits or inspection before continuing.
+
+.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.
+The
+.I -r
+option reverses the order of the commit list.
+With the
+.I -p
+option, instead of printing the commit hashes, the full
+path to their
+.B git/fs
+path is printed. With the
+.I -c
+option, the query must resolve to two commits. The blobs
+that have changed in the commits are 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.
+
+.PP
+.B Git/compat
+spawns an rc subshell with a compatibility stub in
+.IR $path .
+This compatibility stub provides enough of the unix
+.I git
+commands to run tools like
+.I go get
+but not much more.
+
+.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 reachable from
+.B b
+but not reachable from
+.B a.
+
+.SH PROTOCOLS
+.PP
+Git9 supports URL schemes of the format
+.BR transport://dial/repo/path .
+The transport portion specifies the protocol to use.
+If the transport portion is omitted, then the transport used is
+.BR ssh .
+The
+.I dial
+portion is either a plan 9 dial string, or a conventional
+.I host:port
+pair.
+For the ssh protocol, it may also include a
+.I user@
+prefix.
+.I repo/path
+portion is the path of the repository on the server.
+
+The supported transports are
+.B ssh://, git://, hjgit://, gits://, http://,
+and
+.BR https .
+Two of these are specific to git9:
+.I gits://
+and
+.IR hjgit:// .
+Both are the
+.I git://
+protocol, tunnelled over tls.
+.I Hjgit://
+authenticates with the server using Plan 9 authentication,
+using
+.IR tlsclient\ -a .
+Any of these protocol names may be prefixed with
+.IR git+ ,
+for copy-paste compatibility with Unix git.
+
+.SH EXAMPLES
+
+.PP
+In order to create a new repository, run
+.B git/init:
+.PP
+.EX
+git/init myrepo
+.EE
+
+.PP
+To clone an existing repository from a git server, run:
+.PP
+.EX
+git/clone git://github.com/Harvey-OS/harvey
+cd harvey
+# edit files
+git/commit foo.c
+git/push
+.EE
+
+.PP
+To set a user and email for commits, run:
+.PP
+.EX
+% mkdir $home/lib/git
+% >$home/lib/git/config echo '
+[user]
+        name = Ori Bernstein
+        email = ori@eigenstate.org'
+.EE
+
+.SH FILES
+.TP
+$repo/.git
+The full git repository.
+.TP
+$repo/.git/config
+The configuration file for a repository.
+.TP
+$home/lib/git/config
+The global configuration for git.
+The contents of this file are used as fallbacks for the per-repository config.
+
+.SH SEE ALSO
+.IR hg (1)
+.IR replica (1)
+.IR patch (1)
+.IR gitfs (4)
+.IR diff3
+
+.SH BUGS
+.PP
+Repositories with submodules are effectively read-only.
+.PP
+There are a some of missing commands, features, and tools, such as git/rebase
+.PP
+git/compat only works within a git repository.
--- /dev/null
+++ b/sys/man/4/gitfs
@@ -1,0 +1,112 @@
+.TH GITFS 4
+.SH NAME
+git/fs \- git file server
+
+.SH SYNOPSIS
+
+git/fs
+[
+.B -d
+]
+[
+.B -m
+.I mtpt
+]
+
+.SH DESCRIPTION
+
+.PP
+Git/fs serves a file system interface to a git repository in the
+current directory.
+This file system provides a read-only view into the repository contents.
+By default, it is mounted on
+.B /mnt/git.
+It does not cache mutable data, so any changes to the git repository will immediately be reflected in git/fs.
+
+.PP
+Git/fs serves a few levels of hierarchy.
+The top level contains the following files and directories:
+
+.TP
+.B branch
+Exposes branches. Branches are aliases for commit objects.
+
+.TP
+.B object
+Exposes all objects in the git repository.
+Objects may be commits, trees, or blobs.
+
+.TP
+.B HEAD
+This is an alias for the current commit.
+
+.PP
+Commits are also represented as small hierarchies. They contain
+the following files:
+
+.TP
+.B author
+This is the author of the commit.
+The contents of this file are free-form text, but conventionally
+they take the form
+.B Full Name <email@domain.here>
+
+.TP
+.B hash
+The commit id of the current branch
+
+.TP
+.B msg
+The full text of the commit message.
+
+.TP
+.B parent
+The list of parent commit ids of the current commit.
+One parent is listed per line.
+
+.TP
+.B tree
+A directory containing the tree associated with the
+commit.
+The timestamp of the files contained within this
+hierarchy are the same as the date of the commit.
+
+.PP
+Trees are presented as directory listings, and blobs
+as files.
+
+.SH FILES
+.TP
+.B .git
+The git repository being expected.
+.TP
+.B .git/HEAD
+A reference to the current HEAD.
+Used to populate
+.B /mnt/git/HEAD
+.TP
+.git/config
+The per-repository configuation for git tools.
+.TP
+.B $home/lib/git/config
+The global configuration for git tools.
+
+.SH SOURCE
+.TP
+.B /sys/src/cmd/git/fs.c
+
+.SH "SEE ALSO"
+.IR git (1)
+.IR hg (1)
+.IR hgfs (4)
+
+.SH BUGS
+Symlinks are only partially supported.
+Symlinks are treated as regular files when reading.
+Modifying symlinks is unsupported.
+
+.PP
+There is no way to inspect the raw objects. This is
+a feature that would be useful for debugging.
+
+
--- /dev/null
+++ b/sys/src/cmd/git/add
@@ -1,0 +1,39 @@
+#!/bin/rc -e
+rfork ne
+. /sys/lib/git/common.rc
+
+gitup
+
+flagfmt='r:remove'; args='file ...'
+eval `''{aux/getflags $*} || exec aux/usage
+
+add='tracked'
+del='removed'
+if(~ $remove 1){
+	add='removed'
+	del='tracked'
+}
+if(~ $#* 0)
+	exec aux/usage
+
+if(~ $add tracked)
+	files=`$nl{walk -f $gitrel/$*}
+if not
+	files=`$nl{cd .git/index9/tracked/ && walk -f $gitrel/$*}
+
+for(f in $files){
+	if(! ~ `{cleanname $f} .git/*){
+		addpath=.git/index9/$add/$f
+		delpath=.git/index9/$del/$f
+		mkdir -p `{basename -d $addpath}
+		mkdir -p `{basename -d $delpath}
+		# We don't want a matching qid, so that
+		# git/walk doesn't think this came from
+		# a checkout.
+		if(! test -e $addpath)
+			if(~ $add 'tracked' || test -e /mnt/git/HEAD/tree/$f)
+				touch $addpath
+		rm -f $delpath
+	}
+}
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/branch
@@ -1,0 +1,109 @@
+#!/bin/rc -e
+rfork en
+. /sys/lib/git/common.rc
+
+gitup
+
+flagfmt='a:listall, b:baseref ref, d:delete, n:newbr, s:stay, m:merge'
+args='[branch]'
+eval `''{aux/getflags $*} || exec aux/usage
+
+modified=()
+deleted=()
+
+if(~ $#* 0){
+	if(~ $#listall 0)
+		awk '$1=="branch"{print $2}' < /mnt/git/ctl
+	if not
+		cd .git/refs/ && walk -f heads remotes
+	exit
+}
+if(! ~ $#* 1)
+	exec aux/usage
+
+branch=$1
+if(~ $branch refs/heads/*)
+	new=$name
+if not if(~ $branch heads/*)
+	new=refs/$branch
+if not
+	new=refs/heads/$branch
+
+orig=`{git/query HEAD}
+if (~ $#baseref 1)
+	base=`{git/query $baseref} || exit 'bad base'
+if not if(test -e .git/$new)
+	base=`{git/query $new}
+if not
+	base=`{git/query HEAD}
+
+modified=`$nl{git/query -c HEAD $base | grep '^[^-]' | subst '^..'}
+deleted=`$nl{git/query -c HEAD $base | grep '^-' | subst '^..'}
+
+if(! ~ $#modified 0 || ! ~ $#deleted 0 && ~ $#merge 0){
+	git/walk -fRMA $modified $deleted || 
+		die 'uncommited changes would be clobbered'
+}
+if(~ $delete 1){
+	rm -f .git/$new
+	echo 'deleted branch' $new
+	exit
+}
+if(~ $#newbr 0){
+	if(! ~ $#baseref 0)
+		die update would clobber $branch with $baseref
+	baseref=`$nl{echo -n $new | sed s@refs/heads/@refs/remotes/origin/@}
+	if(! test -e .git/$new)
+		if(! base=`{git/query $baseref})
+			die could not find branch $branch
+}
+commit=`{git/query $base} || die 'branch does not exist:' $base
+if(~ $new */*)
+	mkdir -p .git/`{basename -d $new}
+echo $commit > .git/$new
+if(! ~ $#stay 0)
+	exit
+
+basedir=`{git/query -p $base}
+dirtypaths=()
+cleanpaths=($modified $deleted)
+if(! ~ $#modified 0 || ! ~ $#deleted 0)
+	dirtypaths=`$nl{git/walk -cfRMA $modified $deleted}
+if(! ~ $#dirtypaths 0){
+	x=$nl^$cleanpaths
+	y=$nl^$dirtypaths
+	cleanpaths=`$nl{echo $"x$nl$"y | sort | uniq -u}
+}
+for(m in $cleanpaths){
+	d=`{basename -d $m}
+	mkdir -p $d
+	mkdir -p .git/index9/tracked/$d
+	# Modifications can turn a file into
+	# a directory, or vice versa, so we
+	# need to delete and copy the files
+	# over.
+	a=`{test -f $m && echo file || echo dir}
+	b=`{test -f $basedir/tree/$m && echo file || echo dir}
+	if(! ~ $a $b){
+		rm -rf $m
+		rm -rf .git/index9/tracked/$m
+	}
+	if(test -f $basedir/tree/$m){
+		cp  $basedir/tree/$m $m
+		walk -eq $m > .git/index9/tracked/$m
+	}
+}
+
+for(ours in $dirtypaths){
+	common=/mnt/git/object/$orig/tree/$ours
+	theirs=/mnt/git/object/$base/tree/$ours
+	merge1 $ours $ours $common $theirs
+}
+
+if(! ~ $#deleted 0){
+	rm -f $deleted
+	rm -f .git/index9/tracked/$deleted
+}
+
+echo ref: $new > .git/HEAD
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/clone
@@ -1,0 +1,115 @@
+#!/bin/rc
+rfork en
+. /sys/lib/git/common.rc
+
+flagfmt='d:debug, b:branch branch'; args='remote [local]'
+eval `''{aux/getflags $*} || exec aux/usage
+if(~ $debug 1)
+	debug=(-d)
+
+remote=`{echo $1 | subst -g '/*$'}
+local=$2
+
+if(~ $#remote 0)
+	exec aux/usage
+if(~ $#local 0)
+	local=`{basename $remote .git}
+if(~ $#branch 1)
+	branchflag=(-b $branch)
+
+if(test -e $local)
+	die 'repository already exists:' $local
+
+fn clone{
+	flag +e
+	mkdir -p $local/.git
+	mkdir -p $local/.git/objects/pack/
+	mkdir -p $local/.git/refs/heads/
+	
+	cd $local
+	
+	>>.git/config {
+		echo '[remote "origin"]'
+		echo '	url='$remote
+	}
+	{git/fetch  $debug $branchflag $remote >[2=3] | awk '
+		BEGIN{
+			headref=""
+			if(ENVIRON["branch"] != "")
+				headref="refs/remotes/origin/"ENVIRON["branch"]
+			headhash=""
+		}
+		/^symref / && headref == "" {
+			if($2 == "HEAD"){
+				gsub("^refs/heads", "refs/remotes/origin", $3)
+				gsub("^refs/tags", "refs/remotes/origin/tags", $3)
+			}
+		}
+		/^remote /{
+			if($2=="HEAD"){
+				headhash=$3
+			}else if(match($2, "^refs/(heads|tags)/")){
+				gsub("^refs/heads", "refs/remotes/origin", $2)
+				if($2 == headref || (headref == "" && $3 == headhash))
+					headref=$2
+				outfile = ".git/" $2
+				outdir = outfile
+				gsub("/?[^/]*/?$", "", outdir)
+				system("mkdir -p "outdir)
+				print $3 > outfile
+				close(outfile)
+			}
+		}
+		END{
+			if(headref != ""){
+				remote = headref;
+				refdir = headref;
+				gsub("/?[^/]*/?$", "", refdir)
+				gsub("^refs/remotes/origin", "refs/heads", headref)
+				system("mkdir -p .git/"refdir);
+				system("cp .git/" remote " .git/" headref)
+				print "ref: " headref > ".git/HEAD"
+			}else if(headhash != ""){
+				print "warning: detached head "headhash > "/fd/2"
+				print headhash > ".git/HEAD"
+			}
+		}
+	'} |[3] tr '\x0d' '\x0a' || die 'could not clone repository'
+
+	tree=/mnt/git/HEAD/tree
+	lbranch=`{git/branch}
+	rbranch=`{echo $lbranch | subst '^heads' 'remotes/origin'}
+	echo checking out repository...
+	if(test -f .git/refs/$rbranch){
+		cp .git/refs/$rbranch .git/refs/$lbranch
+		git/fs
+		@ {builtin cd $tree && tar cif /fd/1 .} | @ {tar xf /fd/0} \
+			|| die 'checkout failed:' $status
+		for(f in `$nl{walk -f $tree | subst '^'$tree'/*'}){
+			if(! ~ $#f 0){
+				idx=.git/index9/tracked/$f
+				mkdir -p `$nl{basename -d $idx}
+				walk -eq $f > $idx
+			}
+		}
+	}
+	if not{
+		echo no default branch >[1=2]
+		echo check out your code with git/branch >[1=2]
+	}
+}
+
+fn sigint {
+	echo cancelled clone $remote: cleaning $local >[1=2]
+	rm -rf $local
+	exit interrupted
+}
+
+@{clone}
+st=$status
+if(! ~ $st ''){
+	echo failed to clone $remote: cleaning $local >[1=2]
+	rm -rf $local
+	exit $st
+}
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/commit
@@ -1,0 +1,150 @@
+#!/bin/rc -e
+rfork ne
+. /sys/lib/git/common.rc
+
+fn whoami{
+	name=`{git/conf user.name}
+	email=`{git/conf user.email}
+	if(test -f /adm/keys.who){
+		if(~ $name '')
+			name=`{awk -F'|' '$1=="'$user'" {x=$3} END{print x}' </adm/keys.who}
+		if(~ $email '')
+			email=`{awk -F'|' '$1=="'$user'" {x=$5} END{print x}' </adm/keys.who}
+	}
+	if(~ $name '')
+		name=glenda
+	if(~ $email '')
+		email=glenda@9front.local
+}
+
+fn findbranch{
+	branch=`{git/branch}
+	if(test -e /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
+		die 'invalid branch:' $branch
+}
+
+# Remove commentary lines.
+# Remove leading and trailing empty lines.
+# Combine consecutive empty lines between paragraphs.
+# Remove trailing spaces from lines.
+# Ensure there's trailing newline.
+fn cleanmsg{
+	awk '
+	/^[ 	]*#/ {next}
+	/^[ 	]*$/ {empty = 1; next}
+
+	wet && empty {printf "\n"}
+	{wet = 1; empty = 0}
+	{sub(/[ 	]+$/, ""); print $0}
+	'
+}
+
+fn editmsg{
+	if(! test -s $msgfile.tmp){
+		>$msgfile.tmp {
+			echo '# Author:' $name '<'$email'>'
+			echo '#'
+			for(p in $parents)
+				echo '# parent:' $p
+			git/walk -fAMR $files | subst -g '^' '# '
+			echo '#'
+			echo '# Commit message:'
+		}
+		edit=1
+	}
+	if(! ~ $#edit 0){
+		giteditor=`{git/conf core.editor}
+		if(~ $#editor 0)
+			editor=$giteditor
+		if(~ $#editor 0)
+			editor=hold
+		$editor $msgfile.tmp
+	}
+	cleanmsg < $msgfile.tmp > $msgfile
+	if(! test -s $msgfile)
+		die 'empty commit message'
+}
+
+fn parents{
+	if(! ~ $#revise 0)
+		parents=`{cat /mnt/git/HEAD/parent}
+	if not if(test -f .git/index9/merge-parents)
+		parents=`{cat .git/index9/merge-parents | sort | uniq}
+	if not if(~ $initial true)
+		parents=()
+	if not
+		parents=`{git/query $branch}
+}
+
+fn commit{
+	msg=`''{cat $msgfile}
+	if(! ~ $#parents 0)
+		pflags='-p'^$parents
+	hash=`{git/save -n $"name -e $"email  -m $"msg $pflags $files || die $status}
+	rm -f .git/index9/merge-parents
+}
+
+fn update{
+	mkdir -p `{basename -d $refpath}
+	# Paranoia: let's not mangle the repo.
+	if(~ $#hash 0)
+		die 'botched commit'
+	echo $branch: $hash
+	echo $hash > $refpath
+	for(f in $files){
+		if(test -e .git/index9/removed/$f || ! test -e $f){
+			rm -f .git/index9/removed/$f
+			rm -f .git/index9/tracked/$f
+		}
+		if not{
+			mkdir -p `{basename -d $f}
+			walk -eq $f > .git/index9/tracked/$f
+		}
+	}
+}
+
+fn sigexit{
+	rm -f $msgfile $msgfile.tmp
+}
+
+gitup
+
+flagfmt='m:msg message, r:revise, e:edit'; args='[file ...]'
+eval `''{aux/getflags $*} || exec aux/usage
+
+msgfile=/tmp/git-msg.$pid
+if(~ $#msg 1)
+	echo $msg >$msgfile.tmp
+if not if(~ $#revise 1){
+	msg=1
+	echo revising commit `{cat /mnt/git/HEAD/hash}
+	cat /mnt/git/HEAD/msg >$msgfile.tmp
+}
+
+files=()
+if(! ~ $#* 0)
+	files=`$nl{git/walk -c `$nl{cleanname $gitrel/$*}}
+if(~ $status '' || ~ $#files 0 && ! test -f .git/index9/merge-parents && ~ $#revise 0)
+	die 'nothing to commit' $status
+@{
+	flag e +
+	whoami
+	findbranch
+	parents
+	editmsg
+	commit
+	update
+} || die 'could not commit:' $status
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/compat
@@ -1,0 +1,158 @@
+#!/bin/rc
+
+rfork e
+
+opts=()
+args=()
+
+fn cmd_init{
+	while(~ $#* 0){
+		switch($1){
+		case --bare
+			opts=(-b)
+		case -- 
+			# go likes to use these
+		case -*
+			die unknown command init $*
+		case *
+			args=($args $1)
+		}
+		shift
+	}
+	ls >[1=2]
+	git/init $opts $args
+}
+
+fn cmd_clone{
+	branch=()
+	while( ! ~ $#* 0){
+		switch($1){
+		case -b
+			branch=$2
+			shift
+		case --
+			# go likes to use these
+		case -*
+			die unknown command clone $*
+		case *
+			args=($args $1)
+		}
+		shift
+	}
+	git/clone $opts $args
+	if(~ $#branch 1)
+		git/branch -n -b $1 origin/$1
+}
+
+fn cmd_pull{
+	if(~ $1 -*)
+		die unknown options for pull $*
+	git/pull
+}
+
+fn cmd_fetch{
+	while(~ $#* 0){
+		switch($1){
+		case --all
+			opts=($opts -a)
+		case -f
+			opts=($opts -u $2)
+			shift
+		case --
+ 			# go likes to use these
+		case -*
+			die unknown command clone $*
+		case *
+			args=($args $1)
+		}
+		shift
+	}	
+	git/pull -f $opts
+}
+
+
+fn cmd_checkout{
+	if(~ $1 -*)
+		die unknown command pull $*
+	if(~ $#* 0)
+		die git checkout branch
+	git/branch $b
+}
+
+fn cmd_submodule {
+	if(test -f .gitmodules)
+		die 'submodules unsupported'
+}
+
+fn cmd_rev-parse{
+	while(~ $1 -*){
+		switch($1){
+		case --git-dir
+			echo $gitroot/.git
+			shift
+		case --abbrev-ref
+			echo `{dcmd git9/branch | sed s@^heads/@@g}
+			shift
+		case *
+			dprint option $opt
+		}
+		shift
+	}
+}
+
+fn cmd_show-ref{
+	if(~ $1 -*)
+		die unknown command pull $*
+	filter=cat
+	if(~ $#* 0)
+		filter=cat
+	if not
+		filter='-e(^|/)'^$*^'$'
+	for(b in `$nl{cd $gitroot/.git/refs/ && walk -f})
+		echo `{cat $gitroot/.git/refs/$b} refs/$b 
+}
+
+fn cmd_remote{
+	if({! ~ $#* 3 && ! ~ $#* 4} || ! ~ $1 add)
+		die unimplemented remote cmd $*
+	name=$2
+	url=$3
+	if(~ $3 '--')
+		url=$4
+	>>$gitroot/.git/config{
+		echo '[remote "'$name'"]'
+		echo '	url='$url
+	}
+}
+
+fn cmd_version{
+	echo git version 2.2.0
+}
+
+
+fn usage{
+	echo 'git <command> <args>' >[1=2]
+	exit usage
+}
+
+fn die {
+	>[1=2] echo git $_cmdname: $*
+	exit $_cmdname: $*
+}
+
+_cmdname=$1
+if(~ $0 *compat){
+	ramfs -m /n/gitcompat
+	touch /n/gitcompat/git
+	bind $0 /n/gitcompat/git
+	path=( /n/gitcompat $path )
+	exec rc
+}
+
+if(! test -f '/env/fn#cmd_'$1)
+	die git $1: commmand not implemented
+if(! ~ $1 init && ! ~ $1 clone)
+	gitroot=`{git/conf -r} || die repo
+
+cmd_$1 $*(2-)
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/conf.c
@@ -1,0 +1,97 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+int	findroot;
+int	showall;
+int	nfile;
+char	*file[32];
+
+static int
+showconf(char *cfg, char *sect, char *key)
+{
+	char *ln, *p;
+	Biobuf *f;
+	int foundsect, nsect, nkey, found;
+
+	if((f = Bopen(cfg, OREAD)) == nil)
+		return 0;
+
+	found = 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);
+			found = 1;
+			if(!showall){
+				free(ln);
+				goto done;
+			}
+		}
+		free(ln);
+	}
+done:
+	return found;
+}
+
+
+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 repo[512], *p, *s;
+	int i, j;
+
+	ARGBEGIN{
+	case 'f':	file[nfile++]=EARGF(usage());	break;
+	case 'r':	findroot++;			break;
+	case 'a':	showall++;			break;
+	default:	usage();			break;
+	}ARGEND;
+
+	if(findroot){
+		if(findrepo(repo, sizeof(repo)) == -1)
+			sysfatal("%r");
+		print("%s\n", repo);
+		exits(nil);
+	}
+	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/sys/src/cmd/git/delta.c
@@ -1,0 +1,219 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+enum {
+	Minchunk	= 128,
+	Maxchunk	= 8192,
+	Splitmask	= (1<<8)-1,
+	
+};
+
+static u32int geartab[] = {
+    0x67ed26b7, 0x32da500c, 0x53d0fee0, 0xce387dc7, 0xcd406d90, 0x2e83a4d4, 0x9fc9a38d, 0xb67259dc,
+    0xca6b1722, 0x6d2ea08c, 0x235cea2e, 0x3149bb5f, 0x1beda787, 0x2a6b77d5, 0x2f22d9ac, 0x91fc0544,
+    0xe413acfa, 0x5a30ff7a, 0xad6fdde0, 0x444fd0f5, 0x7ad87864, 0x58c5ff05, 0x8d2ec336, 0x2371f853,
+    0x550f8572, 0x6aa448dd, 0x7c9ddbcf, 0x95221e14, 0x2a82ec33, 0xcbec5a78, 0xc6795a0d, 0x243995b7,
+    0x1c909a2f, 0x4fded51c, 0x635d334b, 0x0e2b9999, 0x2702968d, 0x856de1d5, 0x3325d60e, 0xeb6a7502,
+    0xec2a9844, 0x0905835a, 0xa1820375, 0xa4be5cab, 0x96a6c058, 0x2c2ccd70, 0xba40fce3, 0xd794c46b,
+    0x8fbae83e, 0xc3aa7899, 0x3d3ff8ed, 0xa0d42b5b, 0x571c0c97, 0xd2811516, 0xf7e7b96c, 0x4fd2fcbd,
+    0xe2fdec94, 0x282cc436, 0x78e8e95c, 0x80a3b613, 0xcfbee20c, 0xd4a32d1c, 0x2a12ff13, 0x6af82936,
+    0xe5630258, 0x8efa6a98, 0x294fb2d1, 0xdeb57086, 0x5f0fddb3, 0xeceda7ce, 0x4c87305f, 0x3a6d3307,
+    0xe22d2942, 0x9d060217, 0x1e42ed02, 0xb6f63b52, 0x4367f39f, 0x055cf262, 0x03a461b2, 0x5ef9e382,
+    0x386bc03a, 0x2a1e79c7, 0xf1a0058b, 0xd4d2dea9, 0x56baf37d, 0x5daff6cc, 0xf03a951d, 0xaef7de45,
+    0xa8f4581e, 0x3960b555, 0xffbfff6d, 0xbe702a23, 0x8f5b6d6f, 0x061739fb, 0x98696f47, 0x3fd596d4,
+    0x151eac6b, 0xa9fcc4f5, 0x69181a12, 0x3ac5a107, 0xb5198fe7, 0x96bcb1da, 0x1b5ddf8e, 0xc757d650,
+    0x65865c3a, 0x8fc0a41a, 0x87435536, 0x99eda6f2, 0x41874794, 0x29cff4e8, 0xb70efd9a, 0x3103f6e7,
+    0x84d2453b, 0x15a450bd, 0x74f49af1, 0x60f664b1, 0xa1c86935, 0xfdafbce1, 0xe36353e3, 0x5d9ba739,
+    0xbc0559ba, 0x708b0054, 0xd41d808c, 0xb2f31723, 0x9027c41f, 0xf136d165, 0xb5374b12, 0x9420a6ac,
+    0x273958b6, 0xe6c2fad0, 0xebdc1f21, 0xfb33af8b, 0xc71c25cd, 0xe9a2d8e5, 0xbeb38a50, 0xbceb7cc2,
+    0x4e4e73f0, 0xcd6c251d, 0xde4c032c, 0x4b04ac30, 0x725b8b21, 0x4eb8c33b, 0x20d07b75, 0x0567aa63,
+    0xb56b2bb7, 0xc1f5fd3a, 0xcafd35ca, 0x470dd4da, 0xfe4f94cd, 0xfb8de424, 0xe8dbcf40, 0xfe50a37a,
+    0x62db5b5d, 0xf32f4ab6, 0x2c4a8a51, 0x18473dc0, 0xfe0cbb6e, 0xfe399efd, 0xdf34ecc9, 0x6ccd5055,
+    0x46097073, 0x139135c2, 0x721c76f6, 0x1c6a94b4, 0x6eee014d, 0x8a508e02, 0x3da538f5, 0x280d394f,
+    0x5248a0c4, 0x3ce94c6c, 0x9a71ad3a, 0x8493dd05, 0xe43f0ab6, 0x18e4ed42, 0x6c5c0e09, 0x42b06ec9,
+    0x8d330343, 0xa45b6f59, 0x2a573c0c, 0xd7fd3de6, 0xeedeab68, 0x5c84dafc, 0xbbd1b1a8, 0xa3ce1ad1,
+    0x85b70bed, 0xb6add07f, 0xa531309c, 0x8f8ab852, 0x564de332, 0xeac9ed0c, 0x73da402c, 0x3ec52761,
+    0x43af2f4d, 0xd6ff45c8, 0x4c367462, 0xd553bd6a, 0x44724855, 0x3b2aa728, 0x56e5eb65, 0xeaf16173,
+    0x33fa42ff, 0xd714bb5d, 0xfbd0a3b9, 0xaf517134, 0x9416c8cd, 0x534cf94f, 0x548947c2, 0x34193569,
+    0x32f4389a, 0xfe7028bc, 0xed73b1ed, 0x9db95770, 0x468e3922, 0x0440c3cd, 0x60059a62, 0x33504562,
+    0x2b229fbd, 0x5174dca5, 0xf7028752, 0xd63c6aa8, 0x31276f38, 0x0646721c, 0xb0191da8, 0xe00e6de0,
+    0x9eac1a6e, 0x9f7628a5, 0xed6c06ea, 0x0bb8af15, 0xf119fb12, 0x38693c1c, 0x732bc0fe, 0x84953275,
+    0xb82ec888, 0x33a4f1b3, 0x3099835e, 0x028a8782, 0x5fdd51d7, 0xc6c717b3, 0xb06caf71, 0x17c8c111,
+    0x61bad754, 0x9fd03061, 0xe09df1af, 0x3bc9eb73, 0x85878413, 0x9889aaf2, 0x3f5a9e46, 0x42c9f01f,
+    0x9984a4f4, 0xd5de43cc, 0xd294daed, 0xbecba2d2, 0xf1f6e72c, 0x5551128a, 0x83af87e2, 0x6f0342ba,
+};
+
+static u64int
+hash(void *p, int n)
+{
+	uchar buf[SHA1dlen];
+	sha1((uchar*)p, n, buf, nil);
+	return GETBE64(buf);
+}
+
+static void
+addblk(Dtab *dt, void *buf, int len, int off, u64int h)
+{
+	int i, sz, probe;
+	Dblock *db;
+
+	probe = h % dt->sz;
+	while(dt->b[probe].buf != nil){
+		if(len == dt->b[probe].len && memcmp(buf, dt->b[probe].buf, len) == 0)
+			return;
+		probe = (probe + 1) % dt->sz;
+	}
+	assert(dt->b[probe].buf == nil);
+	dt->b[probe].buf = buf;
+	dt->b[probe].len = len;
+	dt->b[probe].off = off;
+	dt->b[probe].hash = h;
+	dt->nb++;
+	if(dt->sz < 2*dt->nb){
+		sz = dt->sz;
+		db = dt->b;
+		dt->sz *= 2;
+		dt->nb = 0;
+		dt->b = eamalloc(dt->sz, sizeof(Dblock));
+		for(i = 0; i < sz; i++)
+			if(db[i].buf != nil)
+				addblk(dt, db[i].buf, db[i].len, db[i].off, db[i].hash);
+		free(db);
+	}		
+}
+
+static Dblock*
+lookup(Dtab *dt, uchar *p, int n)
+{
+	int probe;
+	u64int h;
+
+	h = hash(p, n);
+	for(probe = h % dt->sz; dt->b[probe].buf != nil; probe = (probe + 1) % dt->sz){
+		if(dt->b[probe].hash != h)
+			continue;
+		if(n != dt->b[probe].len)
+			continue;
+		if(memcmp(p, dt->b[probe].buf, n) != 0)
+			continue;
+		return &dt->b[probe];
+	}
+	return nil;
+}
+
+static int
+nextblk(uchar *s, uchar *e)
+{
+	u32int gh;
+	uchar *p;
+
+	if((e - s) < Minchunk)
+		return e - s;
+	p = s + Minchunk;
+	if((e - s) > Maxchunk)
+		e = s + Maxchunk;
+	gh = 0;
+	while(p != e){
+		gh = (gh<<1) + geartab[*p++];
+		if((gh & Splitmask) == 0)
+			break;
+	}
+	return p - s;
+}
+
+void
+dtinit(Dtab *dt, Object *obj)
+{
+	uchar *s, *e;
+	u64int h;
+	vlong n, o;
+	
+	o = 0;
+	s = (uchar*)obj->data;
+	e = s + obj->size;
+	dt->o = ref(obj);
+	dt->nb = 0;
+	dt->sz = 128;
+	dt->b = eamalloc(dt->sz, sizeof(Dblock));
+	dt->base = (uchar*)obj->data;
+	dt->nbase = obj->size;
+	while(s != e){
+		n = nextblk(s, e);
+		h = hash(s, n);
+		addblk(dt, s, n, o, h);
+		s += n;
+		o += n;
+	}
+}
+
+void
+dtclear(Dtab *dt)
+{
+	unref(dt->o);
+	free(dt->b);
+}
+
+static int
+emitdelta(Delta **pd, int *nd, int cpy, int off, int len)
+{
+	Delta *d;
+
+	*nd += 1;
+	*pd = earealloc(*pd, *nd, sizeof(Delta));
+	d = &(*pd)[*nd - 1];
+	d->cpy = cpy;
+	d->off = off;
+	d->len = len;
+	return len;
+}
+
+static int
+stretch(Dtab *dt, Dblock *b, uchar *s, uchar *e, int n)
+{
+	uchar *p, *q, *eb;
+
+	if(b == nil)
+		return n;
+	p = s + n;
+	q = dt->base + b->off + n;
+	eb = dt->base + dt->nbase;
+	while(n < (1<<24)-1){
+		if(p == e || q == eb)
+			break;
+		if(*p != *q)
+			break;
+		p++;
+		q++;
+		n++;
+	}
+	return n;
+}
+
+Delta*
+deltify(Object *obj, Dtab *dt, int *pnd)
+{
+	Delta *d;
+	Dblock *b;
+	uchar *s, *e;
+	vlong n, o;
+	
+	o = 0;
+	d = nil;
+	s = (uchar*)obj->data;
+	e = s + obj->size;
+	*pnd = 0;
+	while(s != e){
+		n = nextblk(s, e);
+		b = lookup(dt, s, n);
+		n = stretch(dt, b, s, e, n);
+		if(b != nil)
+			emitdelta(&d, pnd, 1, b->off, n);
+		else
+			emitdelta(&d, pnd, 0, o, n);
+		s += n;
+		o += n;
+	}
+	return d;
+}
--- /dev/null
+++ b/sys/src/cmd/git/diff
@@ -1,0 +1,37 @@
+#!/bin/rc
+rfork ne
+. /sys/lib/git/common.rc
+
+gitup
+
+flagfmt='c:commit branch, s:summarize'; args='[file ...]'
+eval `''{aux/getflags $*} || exec aux/usage
+
+if(~ $#commit 0)
+	commit=HEAD
+
+files=()
+if(! ~ $#* 0)
+	files=`{cleanname $gitrel/$*}
+
+branch=`{git/query -p $commit}
+if(~ $summarize 1){
+	git/walk -fMAR $files
+	exit
+}
+
+fn lsdirty {
+	git/walk -c -fRMA $files
+	if(! ~ $commit HEAD)
+		git/query -c $commit HEAD | subst '^..'
+}
+
+for(f in `$nl{lsdirty | sort | uniq}){
+	orig=$branch/tree/$f
+	if(! test -f $orig)
+		orig=/dev/null
+	if(! test -f $f)
+		f=/dev/null
+	diff -u $orig $f
+}
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/export
@@ -1,0 +1,89 @@
+#!/bin/rc
+rfork ne
+. /sys/lib/git/common.rc
+
+patchname=/tmp/git.patchname.$pid
+patchfile=/tmp/git.patchfile.$pid
+fn sigexit{
+	rm -f $patchname $patchfile
+}
+
+gitup
+
+flagfmt='o:patchdir patchdir'; args='[query]'
+eval `''{aux/getflags $*} || exec aux/usage
+
+if(~ $#patchdir 1 && ! test -d $patchdir)
+	mkdir -p $patchdir
+
+q=$*
+if(~ $#q 0)
+	q=HEAD
+commits=`{git/query $q || die $status}
+n=1
+m=$#commits
+
+
+# sleazy hack: we want to run
+# under rfork m for the web ui,
+# so don't error if we can't mount
+mntgen /mnt/scratch >[2]/dev/null || status=''
+for(c in $commits){
+	cp=`{git/query -p $c}
+	pp=`{git/query -p $c'~'}
+	fc=`$nl{git/query -c $c~ $c | sed 's/^..//'}
+
+	@{
+		rfork n
+		cd /mnt/scratch
+		if(test -d $pp/tree)
+			bind $pp/tree a
+		if(test -d $cp/tree)
+			bind $cp/tree b
+		
+		echo From $c
+		echo From: `{cat $cp/author}
+		echo Date: `{date -um `{mtime $cp/author | awk '{print $1}'}}
+		<$cp/msg awk '
+		NR == 1 {
+			n = ENVIRON["n"]
+			m = ENVIRON["m"]
+			msg=$0
+			if(m > 1)
+				patch = sprintf("[PATCH %d/%d]", n, m)
+			else
+				patch = "[PATCH]"
+			printf "Subject: %s %s\n\n", patch, msg
+			
+			gsub("^[ 	]|[ 	]$", "", msg)
+			gsub("[^a-zA-Z0-9_]+", "-", msg)
+			printf "%.4d-%s.patch", n, msg >ENVIRON["patchname"]
+			next
+		}
+		{
+			print
+		}'
+		echo '---'
+		echo diff `{basename $pp} `{basename $cp}
+		for(f in $fc){
+			a=a/$f
+			if(! test -e $a)
+				a=/dev/null
+			b=b/$f
+			if(! test -e $b)
+				b=/dev/null
+			ape/diff -urN $a $b
+		}
+	} >$patchfile
+	if(~ $#patchdir 0){
+		cat $patchfile
+		! ~ $n $m && echo
+	}
+	if not{
+		f=$patchdir/`{cat $patchname}
+		mv $patchfile $f
+		echo $f
+	}
+	n=`{echo $n + 1 | bc}
+}
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/fetch.c
@@ -1,0 +1,316 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+char *fetchbranch;
+char *upstream = "origin";
+char *packtmp = ".git/objects/pack/fetch.tmp";
+int listonly;
+
+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;
+	}
+
+	r = -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[Pktmax];
+	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;
+}
+
+char *
+matchcap(char *s, char *cap, int full)
+{
+	if(strncmp(s, cap, strlen(cap)) == 0)
+		if(!full || strlen(s) == strlen(cap))
+			return s + strlen(cap);
+	return nil;
+}
+
+void
+handlecaps(char *caps)
+{
+	char *p, *n, *c, *r;
+
+	for(p = caps; p != nil; p = n){
+		n = strchr(p, ' ');
+		if(n != nil)
+			*n++ = 0;
+		if((c = matchcap(p, "symref=", 0)) != nil){
+			if((r = strchr(c, ':')) != nil){
+				*r++ = '\0';
+				print("symref %s %s\n", c, r);
+			}
+		}
+	}
+}
+
+int
+fetchpack(Conn *c, int pfd, char *packtmp)
+{
+	char buf[Pktmax], idxtmp[256], *sp[3];
+	Hash h, *have, *want;
+	int nref, refsz, first;
+	int i, n, req;
+	vlong packsz;
+	Object *o;
+
+	nref = 0;
+	refsz = 16;
+	first = 1;
+	have = eamalloc(refsz, sizeof(have[0]));
+	want = eamalloc(refsz, sizeof(want[0]));
+	while(1){
+		n = readpkt(c, buf, sizeof(buf));
+		if(n == -1)
+			return -1;
+		if(n == 0)
+			break;
+		if(strncmp(buf, "ERR ", 4) == 0)
+			sysfatal("%s", buf + 4);
+
+		if(first && n > strlen(buf))
+			handlecaps(buf + strlen(buf) + 1);
+		first = 0;
+
+		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++;
+	}
+	if(listonly){
+		flushpkt(c);
+		return 0;
+	}
+
+	if(writephase(c) == -1)
+		sysfatal("write: %r");
+	req = 0;
+	for(i = 0; i < nref; i++){
+		if(hasheq(&have[i], &want[i]))
+			continue;
+		if((o = readobject(want[i])) != nil){
+			unref(o);
+			continue;
+		}
+		n = snprint(buf, sizeof(buf), "want %H\n", want[i]);
+		if(writepkt(c, buf, n) == -1)
+			sysfatal("could not send want for %H", want[i]);
+		req = 1;
+	}
+	flushpkt(c);
+	for(i = 0; i < nref; i++){
+		if(hasheq(&have[i], &Zhash))
+			continue;
+		n = snprint(buf, sizeof(buf), "have %H\n", have[i]);
+		if(writepkt(c, buf, n + 1) == -1)
+			sysfatal("could not send have for %H", have[i]);
+	}
+	if(!req)
+		flushpkt(c);
+
+	n = snprint(buf, sizeof(buf), "done\n");
+	if(writepkt(c, buf, n) == -1)
+		sysfatal("write: %r");
+	if(!req)
+		return 0;
+	if(readphase(c) == -1)
+		sysfatal("read: %r");
+	if((n = readpkt(c, buf, sizeof(buf))) == -1)
+		sysfatal("read: %r");
+	buf[n] = 0;
+
+	fprint(2, "fetching...\n");
+	packsz = 0;
+	while(1){
+		n = readn(c->rfd, buf, sizeof buf);
+		if(n == 0)
+			break;
+		if(n == -1 || write(pfd, buf, n) != n)
+			sysfatal("fetch packfile: %r");
+		packsz += n;
+	}
+	closeconn(c);
+	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 [-dl] [-b br] [-u upstream] 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)
+{
+	int pfd;
+	Conn c;
+
+	ARGBEGIN{
+	case 'b':	fetchbranch=EARGF(usage());	break;
+	case 'u':	upstream=EARGF(usage());	break;
+	case 'd':	chattygit++;			break;
+	case 'l':	listonly++;			break;
+	default:	usage();			break;
+	}ARGEND;
+
+	gitinit();
+	if(argc != 1)
+		usage();
+
+	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(gitconnect(&c, argv[0], "upload") == -1)
+		sysfatal("could not dial %s: %r", argv[0]);
+	if(fetchpack(&c, pfd, packtmp) == -1)
+		sysfatal("fetch failed: %r");
+	closeconn(&c);
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/git/fs.c
@@ -1,0 +1,853 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "git.h"
+
+enum {
+	Qroot,
+	Qhead,
+	Qbranch,
+	Qcommit,
+	Qcommitmsg,
+	Qcommitparent,
+	Qcommittree,
+	Qcommitdata,
+	Qcommithash,
+	Qcommitauthor,
+	Qobject,
+	Qctl,
+	Qmax,
+	Internal=1<<7,
+};
+
+typedef struct Gitaux Gitaux;
+typedef struct Crumb Crumb;
+typedef struct Cache Cache;
+typedef struct Uqid Uqid;
+struct Crumb {
+	char	*name;
+	Object	*obj;
+	Qid	qid;
+	int	mode;
+	vlong	mtime;
+};
+
+struct Gitaux {
+	int	ncrumb;
+	Crumb	*crumb;
+	char	*refpath;
+	int	qdir;
+
+	/* For listing object dir */
+	Objlist	*ols;
+	Object	*olslast;
+};
+
+struct Uqid {
+	vlong	uqid;
+
+	vlong	ppath;
+	vlong	oid;
+	int	t;
+	int	idx;
+};
+
+struct Cache {
+	Uqid *cache;
+	int n;
+	int max;
+};
+
+char *qroot[] = {
+	"HEAD",
+	"branch",
+	"object",
+	"ctl",
+};
+
+#define Eperm	"permission denied";
+#define Eexist	"does not exist";
+#define E2long	"path too long";
+#define Enodir	"not a directory";
+#define Erepo	"unable to read repo";
+#define Egreg	"wat";
+#define Ebadobj	"invalid object";
+
+char	gitdir[512];
+char	*username;
+char	*mtpt = "/mnt/git";
+char	**branches = nil;
+Cache	uqidcache[512];
+vlong	nextqid = Qmax;
+
+static Object*	walklink(Gitaux *, char *, int, int, int*);
+
+vlong
+qpath(Crumb *p, int idx, vlong id, vlong t)
+{
+	int h, i;
+	vlong pp;
+	Cache *c;
+	Uqid *u;
+
+	pp = p ? p->qid.path : 0;
+	h = (pp*333 + id*7 + t) & (nelem(uqidcache) - 1);
+	c = &uqidcache[h];
+	u = c->cache;
+	for(i=0; i <c->n ; i++){
+		if(u->ppath == pp && u->oid == id && u->t == t && u->idx == idx)
+			return (u->uqid << 8) | t;
+		u++;
+	}
+	if(c->n == c->max){
+		c->max += c->max/2 + 1;
+		c->cache = erealloc(c->cache, c->max*sizeof(Uqid));
+	}
+	nextqid++;
+	c->cache[c->n] = (Uqid){nextqid, pp, id, t, idx};
+	c->n++;
+	return (nextqid << 8) | t;
+}
+
+static Crumb*
+crumb(Gitaux *aux, int n)
+{
+	if(n < aux->ncrumb)
+		return &aux->crumb[aux->ncrumb - n - 1];
+	return nil;
+}
+
+static void
+popcrumb(Gitaux *aux)
+{
+	Crumb *c;
+
+	if(aux->ncrumb > 1){
+		c = crumb(aux, 0);
+		free(c->name);
+		unref(c->obj);
+		aux->ncrumb--;
+	}
+}
+
+static vlong
+branchid(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){
+		if(aux->refpath)
+			free(aux->refpath);
+		aux->refpath = estrdup(branches[i]);
+	}
+	return i;
+}
+
+static void
+obj2dir(Dir *d, Crumb *c, Object *o, char *name)
+{
+	d->qid = c->qid;
+	d->atime = c->mtime;
+	d->mtime = c->mtime;
+	d->mode = c->mode;
+	d->name = estrdup9p(name);
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	if(o->type == GBlob || o->type == GTag){
+		d->qid.type = 0;
+		d->mode &= 0777;
+		d->length = o->size;
+	}
+
+}
+
+static int
+rootgen(int i, Dir *d, void *p)
+{
+	Crumb *c;
+
+	c = crumb(p, 0);
+	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(nil, i, i, Qroot);
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	d->mtime = c->mtime;
+	return 0;
+}
+
+static int
+branchgen(int i, Dir *d, void *p)
+{
+	Gitaux *aux;
+	Dir *refs;
+	Crumb *c;
+	int n;
+
+	aux = p;
+	c = crumb(aux, 0);
+	refs = nil;
+	d->qid.vers = 0;
+	d->qid.type = QTDIR;
+	d->qid.path = qpath(c, i, branchid(aux, aux->refpath), Qbranch | Internal);
+	d->mode = 0555 | DMDIR;
+	d->uid = estrdup9p(username);
+	d->gid = estrdup9p(username);
+	d->muid = estrdup9p(username);
+	d->mtime = c->mtime;
+	d->atime = c->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;
+	}
+}
+
+static int
+gtreegen(int i, Dir *d, void *p)
+{
+	Object *o, *l, *e;
+	Gitaux *aux;
+	Crumb *c;
+	int m;
+
+	aux = p;
+	c = crumb(aux, 0);
+	e = c->obj;
+	if(i >= e->tree->nent)
+		return -1;
+	m = e->tree->ent[i].mode;
+	if(e->tree->ent[i].ismod)
+		o = emptydir();
+	else if((o = readobject(e->tree->ent[i].h)) == nil)
+		sysfatal("could not read object %H: %r", e->tree->ent[i].h);
+	if(e->tree->ent[i].islink)
+		if((l = walklink(aux, o->data, o->size, 0, &m)) != nil)
+			o = l;
+	d->qid.vers = 0;
+	d->qid.type = o->type == GTree ? QTDIR : 0;
+	d->qid.path = qpath(c, i, o->id, aux->qdir);
+	d->mode = m;
+	d->mode |= (o->type == GTree) ? 0755 : 0644;
+	d->atime = c->mtime;
+	d->mtime = c->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;
+	Crumb *c;
+
+	c = crumb(p, 0);
+	o = c->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 = 0755 | DMDIR;
+		d->name = estrdup9p("tree");
+		d->qid.type = QTDIR;
+		d->qid.path = qpath(c, i, o->id, Qcommittree);
+		break;
+	case 1:
+		d->name = estrdup9p("parent");
+		d->qid.path = qpath(c, i, o->id, Qcommitparent);
+		break;
+	case 2:
+		d->name = estrdup9p("msg");
+		d->qid.path = qpath(c, i, o->id, Qcommitmsg);
+		break;
+	case 3:
+		d->name = estrdup9p("hash");
+		d->qid.path = qpath(c, i, o->id, Qcommithash);
+		break;
+	case 4:
+		d->name = estrdup9p("author");
+		d->qid.path = qpath(c, i, o->id, Qcommitauthor);
+		break;
+	default:
+		return -1;
+	}
+	return 0;
+}
+
+
+static int
+objgen(int i, Dir *d, void *p)
+{
+	Gitaux *aux;
+	Object *o;
+	Crumb *c;
+	char name[64];
+	Objlist *ols;
+	Hash h;
+
+	aux = p;
+	c = crumb(aux, 0);
+	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){
+		snprint(name, sizeof(name), "%H", aux->olslast->hash);
+		obj2dir(d, c, aux->olslast, name);
+		return 0;
+	}
+	while(ols->idx <= i){
+		if(olsnext(ols, &h) == -1)
+			return -1;
+		if((o = readobject(h)) == nil){
+			fprint(2, "corrupt object %H\n", h);
+			return -1;
+		}
+	}
+	if(o != nil){
+		snprint(name, sizeof(name), "%H", o->hash);
+		obj2dir(d, c, o, name);
+		unref(aux->olslast);
+		aux->olslast = ref(o);
+		return 0;
+	}
+	return -1;
+}
+
+static void
+objread(Req *r, Gitaux *aux)
+{
+	Object *o;
+
+	o = crumb(aux, 0)->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:
+		sysfatal("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");
+	if(getwd(gitdir, sizeof(gitdir)) == nil)
+		sysfatal("getwd: %r");
+	aux = emalloc(sizeof(Gitaux));
+	aux->crumb = emalloc(sizeof(Crumb));
+	aux->crumb[0].qid = (Qid){Qroot, 0, QTDIR};
+	aux->crumb[0].obj = nil;
+	aux->crumb[0].mode = DMDIR | 0555;
+	aux->crumb[0].mtime = d->mtime;
+	aux->crumb[0].name = estrdup("/");
+	aux->ncrumb = 1;
+	r->ofcall.qid = (Qid){Qroot, 0, QTDIR};
+	r->fid->qid = r->ofcall.qid;
+	r->fid->aux = aux;
+	respond(r, nil);
+}
+
+static Object*
+walklink(Gitaux *aux, char *link, int nlink, int ndotdot, int *mode)
+{
+	char *p, *e, *path;
+	Object *o, *n;
+	int i;
+
+	path = emalloc(nlink + 1);
+	memcpy(path, link, nlink);
+	cleanname(path);
+
+	o = crumb(aux, ndotdot)->obj;
+	assert(o->type == GTree);
+	for(p = path; *p; p = e){
+		n = nil;
+		e = p + strcspn(p, "/");
+		if(*e == '/')
+			*e++ = '\0';
+		/*
+		 * cleanname guarantees these show up at the start of the name,
+		 * which allows trimming them from the end of the trail of crumbs
+		 * instead of needing to keep track of full parentage.
+		 */
+		if(strcmp(p, "..") == 0)
+			n = crumb(aux, ++ndotdot)->obj;
+		else if(o->type == GTree)
+			for(i = 0; i < o->tree->nent; i++)
+				if(strcmp(o->tree->ent[i].name, p) == 0){
+					*mode = o->tree->ent[i].mode;
+					n = readobject(o->tree->ent[i].h);
+					break;
+				}
+		o = n;
+		if(o == nil)
+			break;
+	}
+	free(path);
+	return o;
+}
+
+static char *
+objwalk1(Qid *q, Object *o, Crumb *p, Crumb *c, char *name, vlong qdir, Gitaux *aux)
+{
+	Object *w, *l;
+	char *e;
+	int i, m;
+
+	w = nil;
+	e = nil;
+	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;
+			m = o->tree->ent[i].mode;
+			w = readobject(o->tree->ent[i].h);
+			if(!w && o->tree->ent[i].ismod)
+				w = emptydir();
+			if(w && o->tree->ent[i].islink)
+				if((l = walklink(aux, w->data, w->size, 1, &m)) != nil)
+					w = l;
+			if(!w)
+				return Ebadobj;
+			q->type = (w->type == GTree) ? QTDIR : 0;
+			q->path = qpath(c, i, w->id, qdir);
+			c->mode = m;
+			c->mode |= (w->type == GTree) ? DMDIR|0755 : 0644;
+			c->obj = w;
+			break;
+		}
+		if(!w)
+			e = Eexist;
+	}else if(o->type == GCommit){
+		q->type = 0;
+		c->mtime = o->commit->mtime;
+		c->mode = 0444;
+		assert(qdir == Qcommit || qdir == Qobject || qdir == Qcommittree || qdir == Qhead);
+		if(strcmp(name, "msg") == 0)
+			q->path = qpath(p, 0, o->id, Qcommitmsg);
+		else if(strcmp(name, "parent") == 0)
+			q->path = qpath(p, 1, o->id, Qcommitparent);
+		else if(strcmp(name, "hash") == 0)
+			q->path = qpath(p, 2, o->id, Qcommithash);
+		else if(strcmp(name, "author") == 0)
+			q->path = qpath(p, 3, o->id, Qcommitauthor);
+		else if(strcmp(name, "tree") == 0){
+			q->type = QTDIR;
+			q->path = qpath(p, 4, o->id, Qcommittree);
+			unref(c->obj);
+			c->mode = DMDIR | 0755;
+			c->obj = readobject(o->commit->tree);
+			if(c->obj == nil)
+				sysfatal("could not read object %H: %r", 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)
+		return nil;
+
+	return readobject(h);
+}
+
+static char*
+gitwalk1(Fid *fid, char *name, Qid *q)
+{
+	char path[128];
+	Gitaux *aux;
+	Crumb *c, *o;
+	char *e;
+	Dir *d;
+	Hash h;
+
+	e = nil;
+	aux = fid->aux;
+	
+	q->vers = 0;
+	if(strcmp(name, "..") == 0){
+		popcrumb(aux);
+		c = crumb(aux, 0);
+		*q = c->qid;
+		fid->qid = *q;
+		return nil;
+	}
+	
+	aux->crumb = realloc(aux->crumb, (aux->ncrumb + 1) * sizeof(Crumb));
+	aux->ncrumb++;
+	c = crumb(aux, 0);
+	o = crumb(aux, 1);
+	memset(c, 0, sizeof(Crumb));
+	c->mode = o->mode;
+	c->mtime = o->mtime;
+		c->obj = o->obj ? ref(o->obj) : nil;
+	
+	switch(QDIR(&fid->qid)){
+	case Qroot:
+		if(strcmp(name, "HEAD") == 0){
+			*q = (Qid){Qhead, 0, QTDIR};
+			c->mode = DMDIR | 0555;
+			c->obj = readref(".git/HEAD");
+		}else if(strcmp(name, "object") == 0){
+			*q = (Qid){Qobject, 0, QTDIR};
+			c->mode = DMDIR | 0555;
+		}else if(strcmp(name, "branch") == 0){
+			*q = (Qid){Qbranch, 0, QTDIR};
+			aux->refpath = estrdup(".git/refs/");
+			c->mode = DMDIR | 0555;
+		}else if(strcmp(name, "ctl") == 0){
+			*q = (Qid){Qctl, 0, 0};
+			c->mode = 0644;
+		}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(o, Qbranch, branchid(aux, path), Qbranch);
+		else if(d && (c->obj = readref(path)) != nil)
+			q->path = qpath(o, Qbranch, c->obj->id, Qcommit);
+		else
+			e = Eexist;
+		free(d);
+		break;
+	case Qobject:
+		if(c->obj){
+			e = objwalk1(q, o->obj, o, c, name, Qobject, aux);
+		}else{
+			if(hparse(&h, name) == -1)
+				return "invalid object name";
+			if((c->obj = readobject(h)) == nil)
+				return "could not read object";
+			if(c->obj->type == GBlob || c->obj->type == GTag){
+				c->mode = 0644;
+				q->type = 0;
+			}else{
+				c->mode = DMDIR | 0755;
+				q->type = QTDIR;
+			}
+			q->path = qpath(o, Qobject, c->obj->id, Qobject);
+			q->vers = 0;
+		}
+		break;
+	case Qhead:
+		e = objwalk1(q, o->obj, o, c, name, Qhead, aux);
+		break;
+	case Qcommit:
+		e = objwalk1(q, o->obj, o, c, name, Qcommit, aux);
+		break;
+	case Qcommittree:
+		e = objwalk1(q, o->obj, o, c, name, Qcommittree, aux);
+		break;
+	case Qcommitparent:
+	case Qcommitmsg:
+	case Qcommitdata:
+	case Qcommithash:
+	case Qcommitauthor:
+	case Qctl:
+		return Enodir;
+	default:
+		return Egreg;
+	}
+
+	c->name = estrdup(name);
+	c->qid = *q;
+	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->ncrumb = oaux->ncrumb;
+	aux->crumb = eamalloc(oaux->ncrumb, sizeof(Crumb));
+	for(i = 0; i < aux->ncrumb; i++){
+		aux->crumb[i] = oaux->crumb[i];
+		aux->crumb[i].name = estrdup(oaux->crumb[i].name);
+		if(aux->crumb[i].obj)
+			aux->crumb[i].obj = ref(oaux->crumb[i].obj);
+	}
+	if(oaux->refpath)
+		aux->refpath = strdup(oaux->refpath);
+	aux->qdir = oaux->qdir;
+	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->ncrumb; i++){
+		if(aux->crumb[i].obj)
+			unref(aux->crumb[i].obj);
+		free(aux->crumb[i].name);
+	}
+	olsfree(aux->ols);
+	free(aux->refpath);
+	free(aux->crumb);
+	free(aux);
+}
+
+static char *
+readctl(Req *r)
+{
+	char data[1024], ref[512], *s, *e;
+	int fd, n;
+
+	if((fd = open(".git/HEAD", OREAD)) == -1)
+		return Erepo;
+	/* empty HEAD is invalid */
+	if((n = readn(fd, ref, sizeof(ref) - 1)) <= 0)
+		return Erepo;
+	close(fd);
+
+	s = ref;
+	ref[n] = 0;
+	if(strncmp(s, "ref:", 4) == 0)
+		s += 4;
+	while(*s == ' ' || *s == '\t')
+		s++;
+	if((e = strchr(s, '\n')) != nil)
+		*e = 0;
+	if(strstr(s, "refs/") == s)
+		s += strlen("refs/");
+
+	snprint(data, sizeof(data), "branch %s\nrepo %s\n", s, gitdir);
+	readstr(r, data);
+	return nil;
+}
+
+static void
+gitread(Req *r)
+{
+	char buf[256], *e;
+	Gitaux *aux;
+	Object *o;
+	Qid *q;
+
+	aux = r->fid->aux;
+	q = &r->fid->qid;
+	o = crumb(aux, 0)->obj;
+	e = nil;
+
+	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:
+		snprint(buf, sizeof(buf), "%s\n", o->commit->author);
+		readstr(r, buf);
+		break;
+	case Qctl:
+		e = readctl(r);
+		break;
+	case Qhead:
+		/* Empty repositories have no HEAD */
+		if(o == nil)
+			r->ofcall.count = 0;
+		else
+			objread(r, aux);
+		break;
+	case Qcommit:
+	case Qcommittree:
+	case Qcommitdata:
+		objread(r, aux);
+		break;
+	default:
+		e = Egreg;
+	}
+	respond(r, e);
+}
+
+static void
+gitstat(Req *r)
+{
+	Gitaux *aux;
+	Crumb *c;
+
+	aux = r->fid->aux;
+	c = crumb(aux, 0);
+	r->d.uid = estrdup9p(username);
+	r->d.gid = estrdup9p(username);
+	r->d.muid = estrdup9p(username);
+	r->d.qid = r->fid->qid;
+	r->d.mtime = c->mtime;
+	r->d.atime = c->mtime;
+	r->d.mode = c->mode;
+	if(c->obj)
+		obj2dir(&r->d, c, c->obj, c->name);
+	else
+		r->d.name = estrdup9p(c->name);
+	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/sys/src/cmd/git/git.h
@@ -1,0 +1,303 @@
+#include <bio.h>
+#include <mp.h>
+#include <libsec.h>
+#include <flate.h>
+#include <regexp.h>
+
+typedef struct Conn	Conn;
+typedef struct Hash	Hash;
+typedef struct Delta	Delta;
+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 Objlist	Objlist;
+typedef struct Dtab	Dtab;
+typedef struct Dblock	Dblock;
+
+enum {
+	Pathmax		= 512,
+	Npackcache	= 32,
+	Hashsz		= 20,
+	Pktmax		= 65536,
+};
+
+enum {
+	GNone	= 0,
+	GCommit	= 1,
+	GTree	= 2,
+	GBlob	= 3,
+	GTag	= 4,
+	GOdelta	= 6,
+	GRdelta	= 7,
+};
+
+enum {
+	Cloaded	= 1 << 0,
+	Cidx	= 1 << 1,
+	Ccache	= 1 << 2,
+	Cexist	= 1 << 3,
+	Cparsed	= 1 << 5,
+	Cthin	= 1 << 6,
+};
+
+enum {
+	ConnGit,
+	ConnGit9,
+	ConnSsh,
+	ConnHttp,
+};
+
+struct Objlist {
+	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 Conn {
+	int type;
+	int rfd;
+	int wfd;
+
+	/* only used by http */
+	int cfd;
+	char *url;	/* note, first GET uses a different url */
+	char *dir;
+	char *direction;
+};
+
+struct Dirent {
+	char *name;
+	int mode;
+	Hash h;
+	char ismod;
+	char islink;
+};
+
+struct Object {
+	/* Git data */
+	Hash	hash;
+	int	type;
+
+	/* Cache */
+	int	id;
+	int	flag;
+	int	refs;
+	Object	*next;
+	Object	*prev;
+
+	/* For indexing */
+	vlong	off;
+	vlong	len;
+	u32int	crc;
+
+	/* Everything below here gets cleared */
+	char	*all;
+	char	*data;
+	/* size excludes header */
+	vlong	size;
+
+	/* Significant win on memory use */
+	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;
+};
+
+struct Dtab {
+	Object	*o;
+	uchar	*base;
+	int	nbase;
+	Dblock	*b;
+	int	nb;
+	int	sz;
+};
+
+struct Dblock {
+	uchar	*buf;
+	int	len;
+	int	off;
+	u64int	hash;
+};
+
+struct Delta {
+	int	cpy;
+	int	off;
+	int	len;
+};
+
+
+#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 isblank(c) \
+	(((c) != '\n') && isspace(c))
+
+extern Reprog	*authorpat;
+extern Objset	objcache;
+extern Hash	Zhash;
+extern int	chattygit;
+extern int	cachemax;
+extern int	interactive;
+
+#pragma varargck type "H" Hash
+#pragma varargck type "T" int
+#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 *);
+int	listrefs(Hash **, char ***);
+Object	*ancestor(Object *, Object *);
+int	findtwixt(Hash *, int, Hash *, int, Object ***, int *);
+Object	*readobject(Hash);
+Object	*clearedobject(Hash, int);
+void	parseobject(Object *);
+int	indexpack(char *, char *, Hash);
+int	writepack(int, Hash*, int, Hash*, int, Hash*);
+int	hasheq(Hash *, Hash *);
+Object	*ref(Object *);
+void	unref(Object *);
+void	cache(Object *);
+Object	*emptydir(void);
+
+/* object sets */
+void	osinit(Objset *);
+void	osclear(Objset *);
+void	osadd(Objset *, Object *);
+int	oshas(Objset *, Hash);
+Object	*osfind(Objset *, Hash);
+
+/* object listing */
+Objlist	*mkols(void);
+int	olsnext(Objlist *, Hash *);
+void	olsfree(Objlist *);
+
+/* util functions */
+#define dprint(lvl, ...) \
+	if(chattygit >= lvl) _dprint(__VA_ARGS__)
+void	_dprint(char *, ...);
+void	*eamalloc(ulong, ulong);
+void	*emalloc(ulong);
+void	*earealloc(void *, ulong, 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 *);
+int	findrepo(char *, int);
+int	showprogress(int, int);
+
+/* packing */
+void	dtinit(Dtab *, Object*);
+void	dtclear(Dtab*);
+Delta*	deltify(Object*, Dtab*, int*);
+
+/* proto handling */
+int	readpkt(Conn*, char*, int);
+int	writepkt(Conn*, char*, int);
+int	flushpkt(Conn*);
+void	initconn(Conn*, int, int);
+int	gitconnect(Conn *, char *, char *);
+int	readphase(Conn *);
+int	writephase(Conn *);
+void	closeconn(Conn *);
--- /dev/null
+++ b/sys/src/cmd/git/import
@@ -1,0 +1,99 @@
+#!/bin/rc
+rfork ne
+. /sys/lib/git/common.rc
+
+diffpath=/tmp/gitimport.$pid.diff
+fn sigexit {
+	rm -f $diffpath
+}
+
+fn apply @{
+	git/fs
+	email=''
+	name=''
+	msg=''
+	parents='-p'^`{git/query HEAD}
+	branch=`{git/branch}
+	if(test -e /mnt/git/branch/$branch/tree)
+		refpath=.git/refs/$branch
+	if not if(test -e /mnt/git/object/$branch/tree)
+		refpath=.git/HEAD
+	if not
+		die 'invalid branch:' $branch
+	awk '
+	BEGIN{
+		state="headers"
+	}
+	state=="headers" && /^From:/ {
+		sub(/^From:[ \t]*/, "", $0);
+		name=$0;
+		email=$0;
+		sub(/[ \t]*<.*$/, "", name);
+		sub(/.*</, "", email);
+		sub(/>/, "", email);
+	}
+	state=="headers" && /^Date:/{
+		sub(/^Date:[ \t]*/, "", $0)
+		date=$0
+	}
+	state=="headers" && /^Subject:/{
+		sub(/^Subject:[ \t]*(\[PATCH( [0-9]+\/[0-9]+)?\])*[ \t]*/, "", $0);
+		gotmsg = 1
+		print > "/env/msg"
+	}
+	state=="headers" && /^$/ {
+		state="body"
+		next
+	}
+	(state=="headers" || state=="body") && (/^diff/ || /^---[ 	]*$/){
+		state="diff"
+	}
+	state=="body" {
+		print > "/env/msg"
+	}
+	state=="diff" {
+		print > ENVIRON["diffpath"]
+	}
+	END{
+		if(state != "diff")
+			exit("malformed patch: " state);
+		if(name == "" || email == "" || date == "" || gotmsg == "")
+			exit("missing headers");
+		printf "%s", name > "/env/name"
+		printf "%s", email > "/env/email"
+		printf "%s", date > "/env/date"
+	}
+	' || die 'could not import:' $status
+
+	# force re-reading env
+	rc -c '
+		echo applying $msg | sed 1q
+		date=`{seconds $date}
+		if(! files=`$nl{ape/patch -Ep1 < $diffpath | grep ''^patching file'' | sed ''s/^patching file `(.*)''''/\1/''})
+			die ''patch failed''
+		for(f in $files){
+			if(test -e $f)
+				git/add $f
+			if not
+				git/add -r $f
+		}
+		git/walk -fRMA $files
+		if(~ $#nocommit 0){
+			hash=`{git/save -n $name -e $email -m $msg -d $date $parents $files}
+			echo $hash > $refpath
+		}
+		status=''''
+	'
+}
+
+gitup
+
+flagfmt='n:nocommit'; args='file ...'
+eval `''{aux/getflags $*} || exec aux/usage
+
+patches=(/fd/0)
+if(! ~ $#* 0)
+	patches=$*
+for(f in $patches)
+	apply < $f || die $status 
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/init
@@ -1,0 +1,38 @@
+#!/bin/rc -e
+rfork ne
+. /sys/lib/git/common.rc
+
+flagfmt='u:upstream upstream,b:branch branch'; args='name'
+eval `''{aux/getflags $*} || exec aux/usage
+
+dir=$1
+if(~ $#dir 0)
+	dir=.
+if(~ $#branch 0)
+	branch=front
+if(test -e $dir/.git)
+	die $dir/.git already exists
+name=`{basename `{cleanname -d `{pwd} $dir}}
+if(~ $#upstream 0){
+	upstream=`{git/conf 'defaults "origin".baseurl'}
+	if(! ~ $#upstream 0)
+		upstream=$upstream/$name
+}
+
+mkdir -p $dir/.git/refs/^(heads remotes)
+>$dir/.git/config {
+	echo '[core]'
+	echo '	repositoryformatversion = p9.0'
+	if(! ~ $#upstream 0){
+		echo '[remote "origin"]'
+		echo '	url = '$upstream
+	}
+	echo '[branch "'$branch'"]'
+	echo '	remote = origin'
+}
+
+>$dir/.git/HEAD {
+	echo ref: refs/heads/$branch
+}
+
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/log.c
@@ -1,0 +1,329 @@
+#include <u.h>
+#include <libc.h>
+#include "git.h"
+
+typedef struct Pfilt Pfilt;
+struct Pfilt {
+	char	*elt;
+	int	show;
+	Pfilt	*sub;
+	int	nsub;
+};
+
+Biobuf	*out;
+char	*queryexpr;
+char	*commitid;
+int	shortlog;
+
+Object	**heap;
+int	nheap;
+int	heapsz;
+Objset	done;
+Pfilt	*pathfilt;
+
+void
+filteradd(Pfilt *pf, char *path)
+{
+	char *p, *e;
+	int i;
+
+	if((e = strchr(path, '/')) != nil)
+		p = smprint("%.*s", (int)(e - path), path);
+	else
+		p = strdup(path);
+
+	while(e != nil && *e == '/')
+		e++;
+	for(i = 0; i < pf->nsub; i++){
+		if(strcmp(pf->sub[i].elt, p) == 0){
+			pf->sub[i].show = pf->sub[i].show || (e == nil);
+			if(e != nil)
+				filteradd(&pf->sub[i], e);
+			free(p);
+			return;
+		}
+	}
+	pf->sub = earealloc(pf->sub, pf->nsub+1, sizeof(Pfilt));
+	pf->sub[pf->nsub].elt = p;
+	pf->sub[pf->nsub].show = (e == nil);
+	pf->sub[pf->nsub].nsub = 0;
+	pf->sub[pf->nsub].sub = nil;
+	if(e != nil)
+		filteradd(&pf->sub[pf->nsub], e);
+	pf->nsub++;
+}
+
+Hash
+lookup(Pfilt *pf, Object *o)
+{
+	int i;
+
+	for(i = 0; i < o->tree->nent; i++)
+		if(strcmp(o->tree->ent[i].name, pf->elt) == 0)
+			return o->tree->ent[i].h;
+	return Zhash;
+}
+
+int
+filtermatch1(Pfilt *pf, Object *t, Object *pt)
+{
+	Object *a, *b;
+	Hash ha, hb;
+	int i, r;
+
+	if(pf->show)
+		return 1;
+	if(t->type != pt->type)
+		return 1;
+	if(t->type != GTree)
+		return 0;
+
+	for(i = 0; i < pf->nsub; i++){
+		ha = lookup(&pf->sub[i], t);
+		hb = lookup(&pf->sub[i], pt);
+		if(hasheq(&ha, &hb))
+			continue;
+		if(hasheq(&ha, &Zhash) || hasheq(&hb, &Zhash))
+			return 1;
+		if((a = readobject(ha)) == nil)
+			sysfatal("read %H: %r", ha);
+		if((b = readobject(hb)) == nil)
+			sysfatal("read %H: %r", hb);
+		r = filtermatch1(&pf->sub[i], a, b);
+		unref(a);
+		unref(b);
+		if(r)
+			return 1;
+	}
+	return 0;
+}
+
+int
+filtermatch(Object *o)
+{
+	Object *t, *p, *pt;
+	int i, r;
+
+	if(pathfilt == nil)
+		return 1;
+	if((t = readobject(o->commit->tree)) == nil)
+		sysfatal("read %H: %r", o->commit->tree);
+	for(i = 0; i < o->commit->nparent; i++){
+		if((p = readobject(o->commit->parent[i])) == nil)
+			sysfatal("read %H: %r", o->commit->parent[i]);
+		if((pt = readobject(p->commit->tree)) == nil)
+			sysfatal("read %H: %r", o->commit->tree);
+		r = filtermatch1(pathfilt, t, pt);
+		unref(p);
+		unref(pt);
+		if(r)
+			return 1;
+	}
+	return 0;
+}
+
+
+static char*
+nextline(char *p, char *e)
+{
+	for(; p != e; p++)
+		if(*p == '\n')
+			break;
+	return p;
+}
+
+static void
+show(Object *o)
+{
+	Tm tm;
+	char *p, *q, *e;
+
+	assert(o->type == GCommit);
+	if(!filtermatch(o))
+		return;
+
+	if(shortlog){
+		p = o->commit->msg;
+		e = p + o->commit->nmsg;
+		q = nextline(p, e);
+		Bprint(out, "%H ", o->hash);
+		Bwrite(out, p, q - p);
+		Bputc(out, '\n');
+	}else{
+		tmtime(&tm, o->commit->mtime, tzload("local"));
+		Bprint(out, "Hash:\t%H\n", o->hash);
+		Bprint(out, "Author:\t%s\n", o->commit->author);
+		Bprint(out, "Date:\t%τ\n", tmfmt(&tm, "WW MMM D hh:mm:ss z YYYY"));
+		Bprint(out, "\n");
+		p = o->commit->msg;
+		e = p + o->commit->nmsg;
+		for(; p != e; p = q){
+			q = nextline(p, e);
+			Bputc(out, '\t');
+			Bwrite(out, p, q - p);
+			Bputc(out, '\n');
+			if(q != e)
+				q++;
+		}
+		Bprint(out, "\n");
+	}
+	Bflush(out);
+}
+
+static void
+showquery(char *q)
+{
+	Object *o;
+	Hash *h;
+	int n, i;
+
+	if((n = resolverefs(&h, q)) == -1)
+		sysfatal("resolve: %r");
+	for(i = 0; i < n; i++){
+		if((o = readobject(h[i])) == nil)
+			sysfatal("read %H: %r", h[i]);
+		show(o);
+		unref(o);
+	}
+	exits(nil);
+}
+
+static void
+qput(Object *o)
+{
+	Object *p;
+	int i;
+
+	if(oshas(&done, o->hash))
+		return;
+	osadd(&done, o);
+	if(nheap == heapsz){
+		heapsz *= 2;
+		heap = earealloc(heap, heapsz, sizeof(Object*));
+	}
+	heap[nheap++] = o;
+	for(i = nheap - 1; i > 0; i = (i-1)/2){
+		o = heap[i];
+		p = heap[(i-1)/2];
+		if(o->commit->mtime < p->commit->mtime)
+			break;
+		heap[i] = p;
+		heap[(i-1)/2] = o;
+	}
+}
+
+static Object*
+qpop(void)
+{
+	Object *o, *t;
+	int i, l, r, m;
+
+	if(nheap == 0)
+		return nil;
+
+	i = 0;
+	o = heap[0];
+	t = heap[--nheap];
+	heap[0] = t;
+	while(1){
+		m = i;
+		l = 2*i+1;
+		r = 2*i+2;
+		if(l < nheap && heap[m]->commit->mtime < heap[l]->commit->mtime)
+			m = l;
+		if(r < nheap && heap[m]->commit->mtime < heap[r]->commit->mtime)
+			m = r;
+		else
+			break;
+		t = heap[m];
+		heap[m] = heap[i];
+		heap[i] = t;
+		i = m;
+	}
+	return o;
+}
+
+static void
+showcommits(char *c)
+{
+	Object *o, *p;
+	int i;
+	Hash h;
+
+	if(c == nil)
+		c = "HEAD";
+	if(resolveref(&h, c) == -1)
+		sysfatal("resolve %s: %r", c);
+	if((o = readobject(h)) == nil)
+		sysfatal("load %H: %r", h);
+	heapsz = 8;
+	heap = eamalloc(heapsz, sizeof(Object*));
+	osinit(&done);
+	qput(o);
+	while((o = qpop()) != nil){
+		show(o);
+		for(i = 0; i < o->commit->nparent; i++){
+			if((p = readobject(o->commit->parent[i])) == nil)
+				sysfatal("load %H: %r", o->commit->parent[i]);
+			qput(p);
+		}
+		unref(o);
+	}
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-s] [-e expr | -c commit] files..\n", argv0);
+	exits("usage");
+}
+	
+void
+main(int argc, char **argv)
+{
+	char path[1024], repo[1024], *p, *r;
+	int i;
+
+	ARGBEGIN{
+	case 'e':
+		queryexpr = EARGF(usage());
+		break;
+	case 'c':
+		commitid = EARGF(usage());
+		break;
+	case 's':
+		shortlog++;
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	if(findrepo(repo, sizeof(repo)) == -1)
+		sysfatal("find root: %r");
+	if(argc != 0){
+		if(getwd(path, sizeof(path)) == nil)
+			sysfatal("getwd: %r");
+		if(strlen(path) < strlen(repo))
+			sysfatal("path changed");
+		p = path + strlen(repo);
+		pathfilt = emalloc(sizeof(Pfilt));
+		for(i = 0; i < argc; i++){
+			r = smprint("./%s/%s", p, argv[i]);
+			cleanname(r);
+			filteradd(pathfilt, r);
+			free(r);
+		}
+	}
+	if(chdir(repo) == -1)
+		sysfatal("chdir: %r");
+
+	gitinit();
+	tmfmtinstall();
+	out = Bfdopen(1, OWRITE);
+	if(queryexpr != nil)
+		showquery(queryexpr);
+	else
+		showcommits(commitid);
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/git/merge
@@ -1,0 +1,47 @@
+#!/bin/rc -e
+rfork ne
+. /sys/lib/git/common.rc
+
+fn merge{
+	ourbr=/mnt/git/object/$1/tree
+	basebr=/mnt/git/object/$2/tree
+	theirbr=/mnt/git/object/$3/tree
+
+	all=`$nl{{git/query -c $1 $2; git/query -c $2 $3} | sed 's/^..//' | \
+		subst -g '^('$ourbr'|'$basebr'|'$theirbr')/*' | sort | uniq}
+	for(f in $all){
+		ours=$ourbr/$f
+		base=$basebr/$f
+		theirs=$theirbr/$f
+		merge1 $f $theirs $base $ours
+	}
+}
+
+gitup
+
+flagfmt=''; args='theirs'
+eval `''{aux/getflags $*} || exec aux/usage
+
+if(! ~ $#* 1)
+	exec aux/usage
+
+theirs=`{git/query $1}
+ours=`{git/query HEAD}
+base=`{git/query $theirs ^ ' ' ^ $ours ^ '@'}
+
+if(~ $base $theirs)
+	die 'nothing to merge, doofus'
+if(! git/walk -q)
+	die 'dirty work tree, refusing to merge'
+if(~ $base $ours){
+	>[1=2] echo 'fast forwarding...'
+	echo $theirs > .git/refs/`{git/branch}
+	git/revert .
+	exit ''
+}
+echo $ours >> .git/index9/merge-parents
+echo $theirs >> .git/index9/merge-parents
+
+merge $ours $base $theirs
+>[1=2] echo 'merge complete: remember to commit'
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/mkfile
@@ -1,0 +1,57 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin/git
+TARG=\
+	conf\
+	fetch\
+	fs\
+	log\
+	query\
+	repack\
+	save\
+	send\
+	serve\
+	walk
+
+RC=\
+	add\
+	branch\
+	clone\
+	commit\
+	compat\
+	diff\
+	export\
+	import\
+	init\
+	merge\
+	pull\
+	push\
+	rebase\
+	revert\
+	rm
+
+OFILES=\
+	delta.$O\
+	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
+	mkdir -p /sys/lib/git
+	for (i in $TARG)
+		mk $MKFLAGS $i.install
+	for (i in $RC)
+		mk $MKFLAGS $i.rcinstall
+
+%.rcinstall:V:
+	cp $stem $BIN/$stem
+	chmod +x $BIN/$stem
--- /dev/null
+++ b/sys/src/cmd/git/objset.c
@@ -1,0 +1,67 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+void
+osinit(Objset *s)
+{
+	s->sz = 16;
+	s->nobj = 0;
+	s->obj = eamalloc(s->sz, sizeof(Hash));
+}
+
+void
+osclear(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)){
+			s->obj[probe] = o;
+			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 = eamalloc(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, Hash h)
+{
+	return osfind(s, h) != nil;
+}
--- /dev/null
+++ b/sys/src/cmd/git/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(Objlist *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(Objlist *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;
+}
+
+Objlist*
+mkols(void)
+{
+	Objlist *ols;
+
+	ols = emalloc(sizeof(Objlist));
+	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(Objlist *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(Objlist *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/sys/src/cmd/git/pack.c
@@ -1,0 +1,1712 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+typedef struct Buf	Buf;
+typedef struct Metavec	Metavec;
+typedef struct Meta	Meta;
+typedef struct Compout	Compout;
+typedef struct Packf	Packf;
+
+#define max(x, y) ((x) > (y) ? (x) : (y))
+
+struct Metavec {
+	Meta	**meta;
+	int	nmeta;
+	int	metasz;
+};
+
+struct Meta {
+	Object	*obj;
+	char	*path;
+	vlong	mtime;
+
+	/* The best delta we picked */
+	Meta	*head;
+	Meta	*prev;
+	Delta	*delta;
+	int	ndelta;
+	int	nchain;
+
+	/* Only used for delta window */
+	Dtab 	dtab;
+
+	/* Only used for writing offset deltas */
+	vlong	off;
+};
+
+struct Compout {
+	Biobuf *bfd;
+	DigestState *st;
+};
+
+struct Buf {
+	int len;
+	int sz;
+	int off;
+	char *data;
+};
+
+struct Packf {
+	char	path[128];
+	char	*idx;
+	vlong	nidx;
+
+	int	refs;
+	Biobuf	*pack;
+	vlong	opentm;
+};
+
+static int	readpacked(Biobuf *, Object *, int);
+static Object	*readidxobject(Biobuf *, Hash, int);
+
+Objset objcache;
+Object *lruhead;
+Object *lrutail;
+int	ncache;
+int	cachemax = 4096;
+Packf	*packf;
+int	npackf;
+int	openpacks;
+
+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|Cparsed);
+}
+
+void
+unref(Object *o)
+{
+	if(!o)
+		return;
+	o->refs--;
+	if(o->refs == 0)
+		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 != nil)
+		o->prev->next = o->next;
+	if(o->next != nil)
+		o->next->prev = o->prev;
+	if(lrutail == o){
+		lrutail = o->prev;
+		if(lrutail != nil)
+			lrutail->next = nil;
+	}else if(lrutail == nil)
+		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 && lrutail != nil){
+		p = lrutail;
+		lrutail = p->prev;
+		if(lrutail != nil)
+			lrutail->next = nil;
+		p->flag &= ~Ccache;
+		p->prev = nil;
+		p->next = nil;
+		unref(p);
+		ncache--;
+	}		
+}
+
+static int
+loadpack(Packf *pf, char *name)
+{
+	char buf[128];
+	int i, ifd;
+	Dir *d;
+
+	memset(pf, 0, sizeof(Packf));
+	snprint(buf, sizeof(buf), ".git/objects/pack/%s.idx", name);
+	snprint(pf->path, sizeof(pf->path), ".git/objects/pack/%s.pack", name);
+	/*
+	 * if we already have the pack open, just
+	 * steal the loaded info
+	 */
+	for(i = 0; i < npackf; i++){
+		if(strcmp(pf->path, packf[i].path) == 0){
+			pf->pack = packf[i].pack;
+			pf->idx = packf[i].idx;
+			pf->nidx = packf[i].nidx;
+			packf[i].idx = nil;
+			packf[i].pack = nil;
+		}
+	}
+	if((ifd = open(buf, OREAD)) == -1)
+		goto error;
+	if((d = dirfstat(ifd)) == nil)
+		goto error;
+	pf->nidx = d->length;
+	pf->idx = emalloc(pf->nidx);
+	if(readn(ifd, pf->idx, pf->nidx) != pf->nidx){
+		free(pf->idx);
+		free(d);
+		goto error;
+	}
+	free(d);
+	return 0;
+
+error:
+	if(ifd != -1)
+		close(ifd);
+	return -1;	
+}
+
+static void
+refreshpacks(void)
+{
+	Packf *pf, *new;
+	int i, n, l, nnew;
+	Dir *d;
+
+	if((n = slurpdir(".git/objects/pack", &d)) == -1)
+		return;
+	nnew = 0;
+	new = eamalloc(n, sizeof(Packf));
+	for(i = 0; i < n; i++){
+		l = strlen(d[i].name);
+		if(l > 4 && strcmp(d[i].name + l - 4, ".idx") != 0)
+			continue;
+		d[i].name[l - 4] = 0;
+		if(loadpack(&new[nnew], d[i].name) != -1)
+			nnew++;
+	}
+	for(i = 0; i < npackf; i++){
+		pf = &packf[i];
+		free(pf->idx);
+		if(pf->pack != nil)
+			Bterm(pf->pack);
+	}
+	free(packf);
+	packf = new;
+	npackf = nnew;
+	free(d);
+}
+
+static Biobuf*
+openpack(Packf *pf)
+{
+	vlong t;
+	int i, best;
+
+	if(pf->pack == nil){
+		if((pf->pack = Bopen(pf->path, OREAD)) == nil)
+			return nil;
+		openpacks++;
+	}
+	if(openpacks == Npackcache){
+		t = pf->opentm;
+		best = -1;
+		for(i = 0; i < npackf; i++){
+			if(packf[i].opentm < t && packf[i].refs > 0){
+				t = packf[i].opentm;
+				best = i;
+			}
+		}
+		if(best != -1){
+			Bterm(packf[best].pack);
+			packf[best].pack = nil;
+			openpacks--;
+		}
+	}
+	pf->opentm = nsec();
+	pf->refs++;
+	return pf->pack;
+}
+
+static void
+closepack(Packf *pf)
+{
+	if(--pf->refs == 0){
+		Bterm(pf->pack);
+		pf->pack = nil;
+	}
+}
+
+static u32int
+crc32(u32int crc, char *b, int nb)
+{
+	static u32int crctab[256] = {
+		0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 
+		0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 
+		0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 
+		0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 
+		0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 
+		0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 
+		0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 
+		0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 
+		0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 
+		0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 
+		0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 
+		0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 
+		0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 
+		0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 
+		0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 
+		0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 
+		0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 
+		0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 
+		0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 
+		0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 
+		0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 
+		0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 
+		0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 
+		0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 
+		0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 
+		0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 
+		0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 
+		0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 
+		0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 
+		0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 
+		0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 
+		0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 
+		0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 
+		0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 
+		0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 
+		0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 
+		0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
+	};
+	int i;
+
+	crc ^=  0xFFFFFFFF;
+	for(i = 0; i < nb; i++)
+		crc = (crc >> 8) ^ crctab[(crc ^ b[i]) & 0xFF];
+	return crc ^ 0xFFFFFFFF;
+}
+
+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 || d->data == nil){
+		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 vlong
+readvint(char *p, char **pp)
+{
+	int s, c;
+	vlong n;
+	
+	s = 0;
+	n = 0;
+	do {
+		c = *p++;
+		n |= (c & 0x7f) << s;
+		s += 7;
+	} while (c & 0x80 && s < 63);
+	*pp = p;
+
+	return n;
+}
+
+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(d != ed){
+		c = *d++;
+		/* copy from base */
+		if(c & 0x80){
+			o = 0;
+			l = 0;
+			/* Offset in base */
+			if(d != ed && (c & 0x01)) o |= (*d++ <<  0) & 0x000000ff;
+			if(d != ed && (c & 0x02)) o |= (*d++ <<  8) & 0x0000ff00;
+			if(d != ed && (c & 0x04)) o |= (*d++ << 16) & 0x00ff0000;
+			if(d != ed && (c & 0x08)) o |= (*d++ << 24) & 0xff000000;
+
+			/* Length to copy */
+			if(d != ed && (c & 0x10)) l |= (*d++ <<  0) & 0x0000ff;
+			if(d != ed && (c & 0x20)) l |= (*d++ <<  8) & 0x00ff00;
+			if(d != ed && (c & 0x40)) l |= (*d++ << 16) & 0xff0000;
+			if(l == 0) l = 0x10000;
+
+			if(o + l > base->size){
+				werrstr("garbled delta: out of bounds copy");
+				return -1;
+			}
+			memmove(r, b + o, l);
+			r += l;
+		/* inline data */
+		}else{
+			if(c > ed - d){
+				werrstr("garbled delta: write past object");
+				return -1;
+			}
+			memmove(r, d, c);
+			d += c;
+			r += c;
+		}
+	}
+	if(r != er){
+		werrstr("truncated delta");
+		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;
+	o->len = Boffset(f) - o->off;
+	if(d == nil || n != nd)
+		goto error;
+	if((b = readidxobject(f, h, flag|Cthin)) == 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;
+
+	d = nil;
+	if((c = Bgetc(f)) == -1)
+		return -1;
+	r = c & 0x7f;
+	while(c & 0x80 && r < (1ULL<<56)){
+		if((c = Bgetc(f)) == -1)
+			return -1;
+		r = ((r + 1)<<7) | (c & 0x7f);
+	}
+
+	if(r > p || r >= (1ULL<<56)){
+		werrstr("junk offset -%lld (from %lld)", r, p);
+		goto error;
+	}
+	if((n = decompress(&d, f, nil)) == -1)
+		goto error;
+	o->len = Boffset(f) - o->off;
+	if(d == nil || n != nd)
+		goto error;
+	if(Bseek(f, p - r, 0) == -1)
+		goto error;
+	memset(&b, 0, sizeof(Object));
+	if(readpacked(f, &b, flag) == -1)
+		goto error;
+	if(applydelta(o, &b, d, nd) == -1)
+		goto error;
+	clear(&b);
+	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;
+	int 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 at %lld", c, p);
+		return -1;
+	}
+	while(c & 0x80){
+		if((c = Bgetc(f)) == -1)
+			return -1;
+		l |= (c & 0x7f) << s;
+		s += 7;
+	}
+	if(l >= (1ULL << 32)){
+		werrstr("object too big");
+		return -1;
+	}
+	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->len = Boffset(f) - o->off;
+		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(char *idx, int nidx, Hash h)
+{
+	int lo, hi, hidx, i, r, nent;
+	vlong o, oo;
+	void *s;
+
+	o = 8;
+	if(nidx < 8 + 256*4)
+		return -1;
+	/*
+	 * 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;
+		hi = GETBE32(idx + o);
+	} else {
+		o += h.h[0]*4 - 4;
+		lo = GETBE32(idx + o);
+		hi = GETBE32(idx + o + 4);
+	}
+	if(hi == lo)
+		goto notfound;
+	nent=GETBE32(idx + 8 + 255*4);
+
+	/*
+	 * Now that we know the range of hashes that the
+	 * entry may exist in, search them
+	 */
+	r = -1;
+	hidx = -1;
+	o = 8 + 256*4;
+	while(lo < hi){
+		hidx = (hi + lo)/2;
+		s = idx + o + hidx*sizeof(h.h);
+		r = memcmp(h.h, s, sizeof(h.h));
+		if(r < 0)
+			hi = hidx;
+		else if(r > 0)
+			lo = hidx + 1;
+		else
+			break;
+	}
+	if(r != 0)
+		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*hidx;		/* Offset offset */
+	if(oo < 0 || oo + 4 > nidx)
+		goto err;
+	i = GETBE32(idx + oo);
+	o = i & 0xffffffffULL;
+	/*
+	 * Large offsets (i.e. 64-bit) are encoded as an index
+	 * into the next table with the MSB bit set.
+	 */
+	if(o & (1ull << 31)){
+		o &= 0x7fffffffULL;
+		oo = 8;				/* Header */
+		oo += 256*4;			/* Fanout table */
+		oo += Hashsz*nent;		/* Hashes */
+		oo += 4*nent;			/* Checksums */
+		oo += 4*nent;			/* 32-bit Offsets */
+		oo += 8*o;			/* 64-bit Offset offset */
+		if(oo < 0 || oo + 8 >= nidx)
+			goto err;
+		o = GETBE64(idx + oo);
+	}
+	return o;
+
+err:
+	werrstr("out of bounds read");
+	return -1;
+notfound:
+	werrstr("not present");
+	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)
+{
+	int m, entsz, nent;
+	Dirent *t, *ent;
+	char *p, *ep;
+
+	p = o->data;
+	ep = p + o->size;
+
+	nent = 0;
+	entsz = 16;
+	ent = eamalloc(entsz, sizeof(Dirent));	
+	o->tree = emalloc(sizeof(Tinfo));
+	while(p != ep){
+		if(nent == entsz){
+			entsz *= 2;
+			ent = earealloc(ent, entsz, sizeof(Dirent));	
+		}
+		t = &ent[nent++];
+		m = strtol(p, &p, 8);
+		if(*p != ' ')
+			sysfatal("malformed tree %H: *p=(%d) %c\n", o->hash, *p, *p);
+		p++;
+		t->mode = m & 0777;	
+		t->ismod = 0;
+		t->islink = 0;
+		if(m == 0160000){
+			t->mode |= DMDIR;
+			t->ismod = 1;
+		}else if(m == 0120000){
+			t->mode = 0;
+			t->islink = 1;
+		}
+		if(m & 0040000)
+			t->mode |= DMDIR;
+		t->name = p;
+		p = memchr(p, 0, ep - p);
+		if(*p++ != 0 ||  ep - p < sizeof(t->h.h))
+			sysfatal("malformed tree %H, remaining %d (%s)", o->hash, (int)(ep - p), p);
+		memcpy(t->h.h, p, sizeof(t->h.h));
+		p += sizeof(t->h.h);
+	}
+	o->tree->ent = ent;
+	o->tree->nent = nent;
+}
+
+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], hbuf[41];
+	Object *obj, *new;
+	int i, r, retried;
+	Biobuf *f;
+	vlong o;
+
+	if((obj = osfind(&objcache, h)) != nil){
+		if(flag & Cidx){
+			/*
+			 * If we're indexing, we need to be careful
+			 * to only return objects within this pack,
+			 * so if the objects exist outside the pack,
+			 * we don't index the wrong copy.
+			 */
+			if(!(obj->flag & Cidx))
+				return nil;
+			if(obj->flag & Cloaded)
+				return obj;
+			o = Boffset(idx);
+			if(Bseek(idx, obj->off, 0) == -1)
+				return nil;
+			if(readpacked(idx, obj, flag) == -1)
+				return nil;
+			if(Bseek(idx, o, 0) == -1)
+				sysfatal("could not restore offset");
+			cache(obj);
+			return obj;
+		}
+		if(obj->flag & Cloaded)
+			return obj;
+	}
+	if(flag & Cthin)
+		flag &= ~Cidx;
+	if(flag & Cidx)
+		return nil;
+	new = nil;
+	if(obj == nil){
+		new = emalloc(sizeof(Object));
+		new->id = objcache.nobj + 1;
+		new->hash = h;
+		obj = new;
+	}
+
+	o = -1;
+	retried = 0;
+retry:
+	for(i = 0; i < npackf; i++){
+		if((o = searchindex(packf[i].idx, packf[i].nidx, h)) != -1){
+			if((f = openpack(&packf[i])) == nil)
+				goto error;
+			if((r = Bseek(f, o, 0)) != -1)
+				r = readpacked(f, obj, flag);
+			closepack(&packf[i]);
+			if(r == -1)
+				goto error;
+			parseobject(obj);
+			cache(obj);
+			return obj;
+		}
+	}
+			
+
+	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 errorf;
+		Bterm(f);
+		parseobject(obj);
+		cache(obj);
+		return obj;
+	}
+
+	if(o == -1){
+		if(retried)
+			goto error;
+		retried = 1;
+		refreshpacks();
+		goto retry;
+	}
+errorf:
+	Bterm(f);
+error:
+	free(new);
+	return nil;
+}
+
+/*
+ * Loads and returns a cached object.
+ */
+Object*
+readobject(Hash h)
+{
+	Object *o;
+
+	if((o = readidxobject(nil, h, 0)) == nil)
+		return nil;
+	parseobject(o);
+	ref(o);
+	return o;
+}
+
+/*
+ * Creates and returns a cached, cleared object
+ * that will get loaded some other time. Useful
+ * for performance if need to mark that a blob
+ * exists, but we don't care about its contents.
+ *
+ * The refcount of the returned object is 0, so
+ * it doesn't need to be unrefed.
+ */
+Object*
+clearedobject(Hash h, int type)
+{
+	Object *o;
+
+	if((o = osfind(&objcache, h)) != nil)
+		return o;
+
+	o = emalloc(sizeof(Object));
+	o->hash = h;
+	o->type = type;
+	osadd(&objcache, o);
+	o->id = objcache.nobj;
+	o->flag |= Cexist;
+	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);
+}
+
+static u32int
+objectcrc(Biobuf *f, Object *o)
+{
+	char buf[8096];
+	int n, r;
+
+	o->crc = 0;
+	Bseek(f, o->off, 0);
+	for(n = o->len; n > 0; n -= r){
+		r = Bread(f, buf, n > sizeof(buf) ? sizeof(buf) : n);
+		if(r == -1)
+			return -1;
+		if(r == 0)
+			return 0;
+		o->crc = crc32(o->crc, buf, r);
+	}
+	return 0;
+}
+
+int
+indexpack(char *pack, char *idx, Hash ph)
+{
+	char hdr[4*3], buf[8];
+	int nobj, npct, nvalid, nbig;
+	int i, n, pct;
+	Object *o, **obj;
+	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;
+	}
+
+	pct = 0;
+	npct = 0;
+	nvalid = 0;
+	nobj = GETBE32(hdr + 8);
+	obj = eamalloc(nobj, sizeof(Object*));
+	valid = eamalloc(nobj, sizeof(char));
+	if(interactive)
+		fprint(2, "indexing %d objects:   0%%", nobj);
+	while(nvalid != nobj){
+		n = 0;
+		for(i = 0; i < nobj; i++){
+			if(valid[i]){
+				n++;
+				continue;
+			}
+			pct = showprogress((npct*100)/nobj, pct);
+			if(obj[i] == nil){
+				o = emalloc(sizeof(Object));
+				o->off = Boffset(f);
+				obj[i] = o;
+			}
+			o = obj[i];
+			/*
+			 * We can seek around when packing delta chains.
+			 * Be extra careful while we don't know where all
+			 * the objects start.
+			 */
+			Bseek(f, o->off, 0);
+			if(readpacked(f, o, Cidx) == -1)
+				continue;
+			sha1((uchar*)o->all, o->size + strlen(o->all) + 1, o->hash.h, nil);
+			valid[i] = 1;
+			cache(o);
+			npct++;
+			n++;
+			if(objectcrc(f, o) == -1)
+				return -1;
+		}
+		if(n == nvalid){
+			sysfatal("fix point reached too early: %d/%d: %r", nvalid, nobj);
+			goto error;
+		}
+		nvalid = n;
+	}
+	if(interactive)
+		fprint(2, "\b\b\b\b100%%\n");
+	Bterm(f);
+
+	st = nil;
+	qsort(obj, nobj, sizeof(Object*), objcmp);
+	if((f = Bopen(idx, OWRITE)) == nil)
+		return -1;
+	if(hwrite(f, "\xfftOc\x00\x00\x00\x02", 8, &st) != 8)
+		goto error;
+	/* fanout table */
+	c = 0;
+	for(i = 0; i < 256; i++){
+		while(c < nobj && (obj[c]->hash.h[0] & 0xff) <= i)
+			c++;
+		PUTBE32(buf, c);
+		hwrite(f, buf, 4, &st);
+	}
+	for(i = 0; i < nobj; i++){
+		o = obj[i];
+		hwrite(f, o->hash.h, sizeof(o->hash.h), &st);
+	}
+
+	for(i = 0; i < nobj; i++){
+		PUTBE32(buf, obj[i]->crc);
+		hwrite(f, buf, 4, &st);
+	}
+
+	nbig = 0;
+	for(i = 0; i < nobj; i++){
+		if(obj[i]->off < (1ull<<31))
+			PUTBE32(buf, obj[i]->off);
+		else{
+			PUTBE32(buf, (1ull << 31) | nbig);
+			nbig++;
+		}
+		hwrite(f, buf, 4, &st);
+	}
+	for(i = 0; i < nobj; i++){
+		if(obj[i]->off >= (1ull<<31)){
+			PUTBE64(buf, obj[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(obj);
+	free(valid);
+	Bterm(f);
+	return 0;
+
+error:
+	free(obj);
+	free(valid);
+	Bterm(f);
+	return -1;
+}
+
+static int
+deltaordercmp(void *pa, void *pb)
+{
+	Meta *a, *b;
+	int cmp;
+
+	a = *(Meta**)pa;
+	b = *(Meta**)pb;
+	if(a->obj->type != b->obj->type)
+		return a->obj->type - b->obj->type;
+	cmp = strcmp(a->path, b->path);
+	if(cmp != 0)
+		return cmp;
+	if(a->mtime != b->mtime)
+		return a->mtime - b->mtime;
+	return memcmp(a->obj->hash.h, b->obj->hash.h, sizeof(a->obj->hash.h));
+}
+
+static int
+writeordercmp(void *pa, void *pb)
+{
+	Meta *a, *b, *ahd, *bhd;
+
+	a = *(Meta**)pa;
+	b = *(Meta**)pb;
+	ahd = (a->head == nil) ? a : a->head;
+	bhd = (b->head == nil) ? b : b->head;
+	if(ahd->mtime != bhd->mtime)
+		return bhd->mtime - ahd->mtime;
+	if(ahd != bhd)
+		return (uintptr)bhd - (uintptr)ahd;
+	if(a->nchain != b->nchain)
+		return a->nchain - b->nchain;
+	return a->mtime - b->mtime;
+}
+
+static void
+addmeta(Metavec *v, Objset *has, Object *o, char *path, vlong mtime)
+{
+	Meta *m;
+
+	if(oshas(has, o->hash))
+		return;
+	osadd(has, o);
+	if(v == nil)
+		return;
+	m = emalloc(sizeof(Meta));
+	m->obj = o;
+	m->path = estrdup(path);
+	m->mtime = mtime;
+
+	if(v->nmeta == v->metasz){
+		v->metasz = 2*v->metasz;
+		v->meta = earealloc(v->meta, v->metasz, sizeof(Meta*));
+	}
+	v->meta[v->nmeta++] = m;
+}
+
+static void
+freemeta(Meta *m)
+{
+	free(m->delta);
+	free(m->path);
+	free(m);
+}
+
+static int
+loadtree(Metavec *v, Objset *has, Hash tree, char *dpath, vlong mtime)
+{
+	Object *t, *o;
+	Dirent *e;
+	char *p;
+	int i, k;
+
+	if(oshas(has, tree))
+		return 0;
+	if((t = readobject(tree)) == nil)
+		return -1;
+	if(t->type != GTree){
+		fprint(2, "load: %H: not tree\n", t->hash);
+		unref(t);
+		return -1;
+	}
+	addmeta(v, has, t, dpath, mtime);
+	for(i = 0; i < t->tree->nent; i++){
+		e = &t->tree->ent[i];
+		if(oshas(has, e->h))
+			continue;
+		if(e->ismod)
+			continue;
+		k = (e->mode & DMDIR) ? GTree : GBlob;
+		o = clearedobject(e->h, k);
+		p = smprint("%s/%s", dpath, e->name);
+		if(k == GBlob)
+			addmeta(v, has, o, p, mtime);
+		else if(loadtree(v, has, e->h, p, mtime) == -1){
+			free(p);
+			return -1;
+		}
+		free(p);
+	}
+	unref(t);
+	return 0;
+}
+
+static int
+loadcommit(Metavec *v, Objset *has, Hash h)
+{
+	Object *c;
+	int r;
+
+	if(osfind(has, h))
+		return 0;
+	if((c = readobject(h)) == nil)
+		return -1;
+	if(c->type != GCommit){
+		fprint(2, "load: %H: not commit\n", c->hash);
+		unref(c);
+		return -1;
+	}
+	addmeta(v, has, c, "", c->commit->ctime);
+	r = loadtree(v, has, c->commit->tree, "", c->commit->ctime);
+	unref(c);
+	return r;
+}
+
+static int
+readmeta(Hash *theirs, int ntheirs, Hash *ours, int nours, Meta ***m)
+{
+	Object **obj;
+	Objset has;
+	int i, nobj;
+	Metavec v;
+
+	*m = nil;
+	osinit(&has);
+	v.nmeta = 0;
+	v.metasz = 64;
+	v.meta = eamalloc(v.metasz, sizeof(Meta*));
+	if(findtwixt(theirs, ntheirs, ours, nours, &obj, &nobj) == -1)
+		sysfatal("load twixt: %r");
+
+	if(nobj == 0)
+		return 0;
+	for(i = 0; i < nours; i++)
+		if(!hasheq(&ours[i], &Zhash))
+			if(loadcommit(nil, &has, ours[i]) == -1)
+				goto out;
+	for(i = 0; i < nobj; i++)
+		if(loadcommit(&v, &has, obj[i]->hash) == -1)
+			goto out;
+	osclear(&has);
+	*m = v.meta;
+	return v.nmeta;
+out:
+	osclear(&has);
+	free(v.meta);
+	return -1;
+}
+
+static int
+deltasz(Delta *d, int nd)
+{
+	int i, sz;
+	sz = 32;
+	for(i = 0; i < nd; i++)
+		sz += d[i].cpy ? 7 : d[i].len + 1;
+	return sz;
+}
+
+static void
+pickdeltas(Meta **meta, int nmeta)
+{
+	Meta *m, *p;
+	Object *o;
+	Delta *d;
+	int i, j, nd, sz, pct, best;
+
+	pct = 0;
+	dprint(1, "picking deltas\n");
+	fprint(2, "deltifying %d objects:   0%%", nmeta);
+	qsort(meta, nmeta, sizeof(Meta*), deltaordercmp);
+	for(i = 0; i < nmeta; i++){
+		m = meta[i];
+		pct = showprogress((i*100) / nmeta, pct);
+		m->delta = nil;
+		m->ndelta = 0;
+		if(m->obj->type == GCommit || m->obj->type == GTag)
+			continue;
+		if((o = readobject(m->obj->hash)) == nil)
+			sysfatal("readobject %H: %r", m->obj->hash);
+		dtinit(&m->dtab, o);
+		if(i >= 11)
+			dtclear(&meta[i-11]->dtab);
+		best = o->size;
+		for(j = max(0, i - 10); j < i; j++){
+			p = meta[j];
+			/* long chains make unpacking slow */
+			if(p->nchain >= 128 || p->obj->type != o->type)
+				continue;
+			d = deltify(o, &p->dtab, &nd);
+			sz = deltasz(d, nd);
+			if(sz + 32 < best){
+				/*
+				 * if we already picked a best delta,
+				 * replace it.
+				 */
+				free(m->delta);
+				best = sz;
+				m->delta = d;
+				m->ndelta = nd;
+				m->nchain = p->nchain + 1;
+				m->prev = p;
+				m->head = p->head;
+				if(m->head == nil)
+					m->head = p;
+			}else
+				free(d);
+		}
+		unref(o);
+	}
+	for(i = max(0, nmeta - 10); i < nmeta; i++)
+		dtclear(&meta[i]->dtab);
+	fprint(2, "\b\b\b\b100%%\n");
+}
+
+static 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;
+}
+
+static int
+compwrite(void *p, void *buf, int n)
+{
+	return hwrite(((Compout *)p)->bfd, buf, n, &((Compout*)p)->st);
+}
+
+static int
+hcompress(Biobuf *bfd, void *buf, int sz, DigestState **st)
+{
+	int r;
+	Buf b ={
+		.off=0,
+		.data=buf,
+		.sz=sz,
+	};
+	Compout o = {
+		.bfd = bfd,
+		.st = *st,
+	};
+
+	r = deflatezlib(&o, compwrite, &b, compread, 6, 0);
+	*st = o.st;
+	return r;
+}
+
+static void
+append(char **p, int *len, int *sz, void *seg, int nseg)
+{
+	if(*len + nseg >= *sz){
+		while(*len + nseg >= *sz)
+			*sz += *sz/2;
+		*p = erealloc(*p, *sz);
+	}
+	memcpy(*p + *len, seg, nseg);
+	*len += nseg;
+}
+
+static int
+encodedelta(Meta *m, Object *o, Object *b, void **pp)
+{
+	char *p, *bp, buf[16];
+	int len, sz, n, i, j;
+	Delta *d;
+
+	sz = 128;
+	len = 0;
+	p = emalloc(sz);
+
+	/* base object size */
+	buf[0] = b->size & 0x7f;
+	n = b->size >> 7;
+	for(i = 1; n > 0; i++){
+		buf[i - 1] |= 0x80;
+		buf[i] = n & 0x7f;
+		n >>= 7;
+	}
+	append(&p, &len, &sz, buf, i);
+
+	/* target object size */
+	buf[0] = o->size & 0x7f;
+	n = o->size >> 7;
+	for(i = 1; n > 0; i++){
+		buf[i - 1] |= 0x80;
+		buf[i] = n & 0x7f;
+		n >>= 7;
+	}
+	append(&p, &len, &sz, buf, i);
+	for(j = 0; j < m->ndelta; j++){
+		d = &m->delta[j];
+		if(d->cpy){
+			n = d->off;
+			bp = buf + 1;
+			buf[0] = 0x81;
+			buf[1] = 0x00;
+			for(i = 0; i < sizeof(buf); i++) {
+				buf[0] |= 1<<i;
+				*bp++ = n & 0xff;
+				n >>= 8;
+				if(n == 0)
+					break;
+			}
+
+			n = d->len;
+			if(n != 0x10000) {
+				buf[0] |= 0x1<<4;
+				for(i = 0; i < sizeof(buf)-4 && n > 0; i++){
+					buf[0] |= 1<<(i + 4);
+					*bp++ = n & 0xff;
+					n >>= 8;
+				}
+			}
+			append(&p, &len, &sz, buf, bp - buf);
+		}else{
+			n = 0;
+			while(n != d->len){
+				buf[0] = (d->len - n < 127) ? d->len - n : 127;
+				append(&p, &len, &sz, buf, 1);
+				append(&p, &len, &sz, o->data + d->off + n, buf[0]);
+				n += buf[0];
+			}
+		}
+	}
+	*pp = p;
+	return len;
+}
+
+static int
+packhdr(char *hdr, int ty, int len)
+{
+	int i;
+
+	hdr[0] = ty << 4;
+	hdr[0] |= len & 0xf;
+	len >>= 4;
+	for(i = 1; len != 0; i++){
+		hdr[i-1] |= 0x80;
+		hdr[i] = len & 0x7f;
+		len >>= 7;
+	}
+	return i;
+}
+
+static int
+packoff(char *hdr, vlong off)
+{
+	int i, j;
+	char rbuf[8];
+
+	rbuf[0] = off & 0x7f;
+	for(i = 1; (off >>= 7) != 0; i++)
+		rbuf[i] = (--off & 0x7f)|0x80;
+
+	j = 0;
+	while(i > 0)
+		hdr[j++] = rbuf[--i];
+	return j;
+}
+
+static int
+genpack(int fd, Meta **meta, int nmeta, Hash *h, int odelta)
+{
+	int i, nh, nd, res, pct, ret;
+	DigestState *st;
+	Biobuf *bfd;
+	Meta *m;
+	Object *o, *b;
+	char *p, buf[32];
+
+	st = nil;
+	ret = -1;
+	pct = 0;
+	dprint(1, "generating pack\n");
+	if((fd = dup(fd, -1)) == -1)
+		return -1;
+	if((bfd = Bfdopen(fd, OWRITE)) == nil)
+		return -1;
+	if(hwrite(bfd, "PACK", 4, &st) == -1)
+		return -1;
+	PUTBE32(buf, 2);
+	if(hwrite(bfd, buf, 4, &st) == -1)
+		return -1;
+	PUTBE32(buf, nmeta);
+	if(hwrite(bfd, buf, 4, &st) == -1)
+		return -1;
+	qsort(meta, nmeta, sizeof(Meta*), writeordercmp);
+	if(interactive)
+		fprint(2, "writing %d objects:   0%%", nmeta);
+	for(i = 0; i < nmeta; i++){
+		pct = showprogress((i*100)/nmeta, pct);
+		m = meta[i];
+		m->off = Boffset(bfd);
+		if((o = readobject(m->obj->hash)) == nil)
+			return -1;
+		if(m->delta == nil){
+			nh = packhdr(buf, o->type, o->size);
+			hwrite(bfd, buf, nh, &st);
+			if(hcompress(bfd, o->data, o->size, &st) == -1)
+				goto error;
+		}else{
+			b = readobject(m->prev->obj->hash);
+			nd = encodedelta(m, o, b, &p);
+			unref(b);
+			if(odelta && m->prev->off != 0){
+				nh = 0;
+				nh += packhdr(buf, GOdelta, nd);
+				nh += packoff(buf+nh, m->off - m->prev->off);
+				hwrite(bfd, buf, nh, &st);
+			}else{
+				nh = packhdr(buf, GRdelta, nd);
+				hwrite(bfd, buf, nh, &st);
+				hwrite(bfd, m->prev->obj->hash.h, sizeof(m->prev->obj->hash.h), &st);
+			}
+			res = hcompress(bfd, p, nd, &st);
+			free(p);
+			if(res == -1)
+				goto error;
+		}
+		unref(o);
+	}
+	if(interactive)
+		fprint(2, "\b\b\b\b100%%\n");
+	sha1(nil, 0, h->h, st);
+	if(Bwrite(bfd, h->h, sizeof(h->h)) == -1)
+		goto error;
+	ret = 0;
+error:
+	if(Bterm(bfd) == -1)
+		return -1;
+	return ret;
+}
+
+int
+writepack(int fd, Hash *theirs, int ntheirs, Hash *ours, int nours, Hash *h)
+{
+	Meta **meta;
+	int i, r, nmeta;
+
+	if((nmeta = readmeta(theirs, ntheirs, ours, nours, &meta)) == -1)
+		return -1;
+	pickdeltas(meta, nmeta);
+	r = genpack(fd, meta, nmeta, h, 0);
+	for(i = 0; i < nmeta; i++)
+		freemeta(meta[i]);
+	free(meta);
+	return r;
+}
--- /dev/null
+++ b/sys/src/cmd/git/proto.c
@@ -1,0 +1,459 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+#define Useragent	"useragent git/2.24.1"
+#define Contenthdr	"headers Content-Type: application/x-git-%s-pack-request"
+#define Accepthdr	"headers Accept: application/x-git-%s-pack-result"
+
+enum {
+	Nproto	= 16,
+	Nport	= 16,
+	Nhost	= 256,
+	Npath	= 128,
+	Nrepo	= 64,
+	Nbranch	= 32,
+};
+
+void
+tracepkt(int v, char *pfx, char *b, int n)
+{
+	char *f;
+	int o, i;
+
+	if(chattygit < v)
+		return;
+	o = 0;
+	f = emalloc(n*4 + 1);
+	for(i = 0; i < n; i++){
+		if(isprint(b[i])){
+			f[o++] = b[i];
+			continue;
+		}
+		f[o++] = '\\';
+		switch(b[i]){
+		case '\\':	f[o++] = '\\';	break;
+		case '\n':	f[o++] = 'n';	break;
+		case '\r':	f[o++] = 'r';	break;
+		case '\v':	f[o++] = 'v';	break;
+		case '\0':	f[o++] = '0';	break;
+		default:
+			f[o++] = 'x';
+			f[o++] = "0123456789abcdef"[(b[i]>>4)&0xf];
+			f[o++] = "0123456789abcdef"[(b[i]>>0)&0xf];
+			break;
+		}
+	}
+	f[o] = '\0';
+	fprint(2, "%s %04x:\t%s\n", pfx, n, f);
+	free(f);
+}
+
+int
+readpkt(Conn *c, char *buf, int nbuf)
+{
+	char len[5];
+	char *e;
+	int n;
+
+	if(readn(c->rfd, len, 4) == -1)
+		return -1;
+	len[4] = 0;
+	n = strtol(len, &e, 16);
+	if(n == 0){
+		dprint(1, "=r=> 0000\n");
+		return 0;
+	}
+	if(e != len + 4 || n <= 4)
+		sysfatal("pktline: bad length '%s'", len);
+	n  -= 4;
+	if(n >= nbuf)
+		sysfatal("pktline: undersize buffer");
+	if(readn(c->rfd, buf, n) != n)
+		return -1;
+	buf[n] = 0;
+	tracepkt(1, "=r=>", buf, n);
+	return n;
+}
+
+int
+writepkt(Conn *c, char *buf, int nbuf)
+{
+	char len[5];
+
+
+	snprint(len, sizeof(len), "%04x", nbuf + 4);
+	if(write(c->wfd, len, 4) != 4)
+		return -1;
+	if(write(c->wfd, buf, nbuf) != nbuf)
+		return -1;
+	tracepkt(1, "<=w=", buf, nbuf);
+	return 0;
+}
+
+int
+flushpkt(Conn *c)
+{
+	dprint(1, "<=w= 0000\n");
+	return write(c->wfd, "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] = 0;
+}
+
+static int
+parseuri(char *uri, char *proto, char *host, char *port, char *path, char *repo)
+{
+	char *s, *p, *q;
+	int n, hasport;
+	print("uri: \"%s\"\n", uri);
+
+	p = strstr(uri, "://");
+	if(p == nil)
+		snprint(proto, Nproto, "ssh");
+	else if(strncmp(uri, "git+", 4) == 0)
+		grab(proto, Nproto, uri + 4, p);
+	else
+		grab(proto, Nproto, uri, p);
+	*port = 0;
+	hasport = 1;
+	if(strcmp(proto, "git") == 0)
+		snprint(port, Nport, "9418");
+	else if(strncmp(proto, "https", 5) == 0)
+		snprint(port, Nport, "443");
+	else if(strncmp(proto, "http", 4) == 0)
+		snprint(port, Nport, "80");
+	else if(strncmp(proto, "hjgit", 5) == 0)
+		snprint(port, Nport, "17021");
+	else if(strncmp(proto, "gits", 5) == 0)
+		snprint(port, Nport, "9419");
+	else
+		hasport = 0;
+	s = (p != nil) ? p + 3 : uri;
+	p = nil;
+	if(!hasport){
+		p = strstr(s, ":");
+		if(p != nil)
+			p++;
+	}
+	if(p == nil)
+		p = strstr(s, "/");
+	if(p == nil || 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(path, Npath, "%s", p);
+	if((q = strrchr(p, '/')) != nil)
+		p = q + 1;
+	if(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;
+}
+
+static int
+webclone(Conn *c, char *url)
+{
+	char buf[16];
+	int n, conn;
+
+	if((c->cfd = open("/mnt/web/clone", ORDWR)) < 0)
+		goto err;
+	if((n = read(c->cfd, buf, sizeof(buf)-1)) == -1)
+		goto err;
+	buf[n] = 0;
+	conn = atoi(buf);
+
+	/* github will behave differently based on useragent */
+	if(write(c->cfd, Useragent, sizeof(Useragent)) == -1)
+		return -1;
+	dprint(1, "open url %s\n", url);
+	if(fprint(c->cfd, "url %s", url) == -1)
+		goto err;
+	free(c->dir);
+	c->dir = smprint("/mnt/web/%d", conn);
+	return 0;
+err:
+	if(c->cfd != -1)
+		close(c->cfd);
+	return -1;
+}
+
+static int
+webopen(Conn *c, char *file, int mode)
+{
+	char path[128];
+	int fd;
+
+	snprint(path, sizeof(path), "%s/%s", c->dir, file);
+	if((fd = open(path, mode)) == -1)
+		return -1;
+	return fd;
+}
+
+static int
+issmarthttp(Conn *c, char *direction)
+{
+	char buf[Pktmax+1], svc[128];
+	int n;
+
+	if((n = readpkt(c, buf, sizeof(buf))) == -1)
+		sysfatal("http read: %r");
+	buf[n] = 0;
+	snprint(svc, sizeof(svc), "# service=git-%s-pack\n", direction);
+	if(strncmp(svc, buf, n) != 0){
+		werrstr("dumb http protocol not supported");
+		return -1;
+	}
+	if(readpkt(c, buf, sizeof(buf)) != 0){
+		werrstr("protocol garble: expected flushpkt");
+		return -1;
+	}
+	return 0;
+}
+
+static int
+dialhttp(Conn *c, char *host, char *port, char *path, char *direction)
+{
+	char *geturl, *suff, *hsep, *psep;
+
+	suff = "";
+	hsep = "";
+	psep = "";
+	if(port && strlen(port) != 0)
+		hsep = ":";
+	if(path && path[0] != '/')
+		psep = "/";
+	memset(c, 0, sizeof(*c));
+	geturl = smprint("https://%s%s%s%s%s%s/info/refs?service=git-%s-pack", host, hsep, port, psep, path, suff, direction);
+	c->type = ConnHttp;
+	c->url = smprint("https://%s%s%s%s%s%s/git-%s-pack", host, hsep, port, psep, path, suff, direction);
+	c->cfd = webclone(c, geturl);
+	free(geturl);
+	if(c->cfd == -1)
+		return -1;
+	c->rfd = webopen(c, "body", OREAD);
+	c->wfd = -1;
+	if(c->rfd == -1)
+		return -1;
+	if(issmarthttp(c, direction) == -1)
+		return -1;
+	c->direction = estrdup(direction);
+	return 0;
+}
+
+static int
+dialssh(Conn *c, char *host, char *, char *path, char *direction)
+{
+	int pid, pfd[2];
+	char cmd[64];
+
+	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);
+		dprint(1, "exec ssh '%s' '%s' %s\n", host, cmd, path);
+		execl("/bin/ssh", "ssh", host, cmd, path, nil);
+	}else{
+		close(pfd[0]);
+		c->type = ConnSsh;
+		c->rfd = pfd[1];
+		c->wfd = dup(pfd[1], -1);
+	}
+	return 0;
+}
+
+static int
+dialhjgit(Conn *c, char *host, char *port, char *path, char *direction, int auth)
+{
+	char *ds, *p, *e, cmd[512];
+	int pid, pfd[2];
+
+	if((ds = netmkaddr(host, "tcp", port)) == nil)
+		return -1;
+	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);
+		dprint(1, "exec tlsclient -a %s\n", ds);
+		if(auth)
+			execl("/bin/tlsclient", "tlsclient", "-a", ds, nil);
+		else
+			execl("/bin/tlsclient", "tlsclient", ds, nil);
+		sysfatal("exec: %r");
+	}else{
+		close(pfd[0]);
+		p = cmd;
+		e = cmd + sizeof(cmd);
+		p = seprint(p, e - 1, "git-%s-pack %s", direction, path);
+		p = seprint(p + 1, e, "host=%s", host);
+		c->type = ConnGit9;
+		c->rfd = pfd[1];
+		c->wfd = dup(pfd[1], -1);
+		if(writepkt(c, cmd, p - cmd + 1) == -1){
+			fprint(2, "failed to write message\n");
+			close(c->rfd);
+			close(c->wfd);
+			return -1;
+		}
+	}
+	return 0;
+}
+
+
+static int
+dialgit(Conn *c, char *host, char *port, char *path, char *direction)
+{
+	char *ds, *p, *e, cmd[512];
+	int fd;
+
+	if((ds = netmkaddr(host, "tcp", port)) == nil)
+		return -1;
+	dprint(1, "dial %s git-%s-pack %s\n", ds, direction, path);
+	fd = dial(ds, nil, nil, nil);
+	if(fd == -1)
+		return -1;
+	p = cmd;
+	e = cmd + sizeof(cmd);
+	p = seprint(p, e - 1, "git-%s-pack %s", direction, path);
+	p = seprint(p + 1, e, "host=%s", host);
+	c->type = ConnGit;
+	c->rfd = fd;
+	c->wfd = dup(fd, -1);
+	if(writepkt(c, cmd, p - cmd + 1) == -1){
+		fprint(2, "failed to write message\n");
+		close(fd);
+		return -1;
+	}
+	return 0;
+}
+
+void
+initconn(Conn *c, int rd, int wr)
+{
+	c->type = ConnGit;
+	c->rfd = rd;
+	c->wfd = wr;
+}
+
+int
+gitconnect(Conn *c, char *uri, char *direction)
+{
+	char proto[Nproto], host[Nhost], port[Nport];
+	char repo[Nrepo], path[Npath];
+
+	if(parseuri(uri, proto, host, port, path, repo) == -1){
+		werrstr("bad uri %s", uri);
+		return -1;
+	}
+
+	memset(c, 0, sizeof(Conn));
+	if(strcmp(proto, "ssh") == 0)
+		return dialssh(c, host, port, path, direction);
+	else if(strcmp(proto, "git") == 0)
+		return dialgit(c, host, port, path, direction);
+	else if(strcmp(proto, "hjgit") == 0)
+		return dialhjgit(c, host, port, path, direction, 1);
+	else if(strcmp(proto, "gits") == 0)
+		return dialhjgit(c, host, port, path, direction, 0);
+	else if(strcmp(proto, "http") == 0 || strcmp(proto, "https") == 0)
+		return dialhttp(c, host, port, path, direction);
+	werrstr("unknown protocol %s", proto);
+	return -1;
+}
+
+int
+writephase(Conn *c)
+{
+	char hdr[128];
+	int n;
+
+	dprint(1, "start write phase\n");
+	if(c->type != ConnHttp)
+		return 0;
+
+	if(c->wfd != -1)
+		close(c->wfd);
+	if(c->cfd != -1)
+		close(c->cfd);
+	if((c->cfd = webclone(c, c->url)) == -1)
+		return -1;
+	n = snprint(hdr, sizeof(hdr), Contenthdr, c->direction);
+	if(write(c->cfd, hdr, n) == -1)
+		return -1;
+	n = snprint(hdr, sizeof(hdr), Accepthdr, c->direction);
+	if(write(c->cfd, hdr, n) == -1)
+		return -1;
+	if((c->wfd = webopen(c, "postbody", OWRITE)) == -1)
+		return -1;
+	c->rfd = -1;
+	return 0;
+}
+
+int
+readphase(Conn *c)
+{
+	dprint(1, "start read phase\n");
+	if(c->type != ConnHttp)
+		return 0;
+	if(close(c->wfd) == -1)
+		return -1;
+	if((c->rfd = webopen(c, "body", OREAD)) == -1)
+		return -1;
+	c->wfd = -1;
+	return 0;
+}
+
+void
+closeconn(Conn *c)
+{
+	close(c->rfd);
+	close(c->wfd);
+	switch(c->type){
+	case ConnGit:
+		break;
+	case ConnGit9:
+	case ConnSsh:
+		free(wait());
+		break;
+	case ConnHttp:
+		close(c->cfd);
+		break;
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/git/pull
@@ -1,0 +1,82 @@
+#!/bin/rc -e
+rfork en
+. /sys/lib/git/common.rc
+
+fn update{
+	branch=$1
+	upstream=$2
+	url=$3
+	dir=$4
+	bflag=()
+	dflag=()
+	if(! ~ $#branch 0)
+		bflag=(-b $branch)
+	if(! ~ $#debug 0)
+		dflag='-d'
+	{git/fetch $dflag $bflag -u $upstream $url >[2=3] || die $status} | awk '
+	/^remote/{
+		if($2=="HEAD")
+			next
+		ref=$2
+		hash=$3
+		gsub("^refs/heads", "refs/remotes/'$upstream'", ref)
+		outfile = ".git/"ref
+		system("mkdir -p `{basename -d "outfile"}");
+		print hash > outfile;
+		close(outfile);
+	}
+	' |[3] tr '\x0d' '\x0a'
+}
+
+gitup
+
+flagfmt='a:allbranch, b:branch branch, d:debug,
+	f:fetchonly, u:upstream upstream, q:quiet'
+args=''
+eval `''{aux/getflags $*} || exec aux/usage
+
+if(~ $#branch 0)
+	branch=refs/`{git/branch}
+if(~ $allbranch 1)
+	branch=''
+
+if(~ $#upstream 0)
+	upstream=origin
+remote=`$nl{git/conf 'remote "'$upstream'".url'}
+if(~ $#remote 0){
+	remote=$upstream
+	upstream=THEM
+}
+
+update $branch $upstream $remote
+if (~ $fetchonly 1)
+	exit
+
+local=`{git/branch}
+remote=`{git/branch | subst '^(refs/)?heads' 'remotes/'$upstream}
+
+# we have local commits, but the remote hasn't changed.
+# in this case, we want to keep the local commits untouched.
+if(~ `{git/query HEAD $remote @} `{git/query $remote}){
+	echo 'up to date' >[1=2]
+	exit
+}
+# The remote repository and our HEAD have diverged: we
+# need to merge.
+if(! ~ `{git/query HEAD $remote @} `{git/query HEAD}){
+	>[1=2]{
+		echo ours:	`{git/query HEAD}
+		echo theirs:	`{git/query $remote}
+		echo common:	`{git/query HEAD $remote @}
+		echo git/merge $remote
+	}
+	exit diverged
+}
+# The remote is directly ahead of the local, and we have
+# no local commits that need merging.
+if(~ $#quiet 0)
+	git/log -s -e $local'..'$remote >[1=2]
+echo
+echo $remote':' `{git/query $local} '=>' `{git/query $remote}  >[1=2]
+git/branch -mnb $remote $local
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/push
@@ -1,0 +1,51 @@
+#!/bin/rc -e
+rfork en
+. /sys/lib/git/common.rc
+
+gitup
+
+flagfmt='a:pushall, b:branch branch, f:force, d:debug,
+         r:remove remove, u:upstream upstream' args=''
+eval `''{aux/getflags $*} || exec aux/usage
+if(! ~ $#* 0)
+	exec aux/usage
+
+if(~ $pushall 1)
+	branch=`$nl{cd .git/refs/heads && walk -f}
+if(~ $#branch 0)
+	branch=`{git/branch}
+if(~ $#branch 0)
+	die 'no branches'
+if(~ $force 1)
+	force=-f
+if(~ $debug 1)
+	debug='-d'
+
+if(~ $#upstream 0)
+	upstream=origin
+
+remotes=`$nl{git/conf -a 'remote "'$upstream'".url'}
+if(~ $#remotes 0)
+	remotes=$upstream
+branch=-b^$branch
+if(! ~ $#remove 0)
+	remove=-r^$remove
+for(remote in $remotes){
+	updates=`$nl{git/send $debug $force $branch $remove $remote || die $status}
+	for(ln in $updates){
+		u=`{echo $ln}
+		refpath=`{echo $u(2) | subst '^refs/heads/' '.git/refs/remotes/'$upstream'/'}
+		switch($u(1)){
+		case update;
+			mkdir -p `{basename -d $refpath}
+			echo $u(4) > $refpath
+			echo $u(2)^':' $u(3) '=>' $u(4)
+		case delete;
+			echo $u(2)^': removed'
+			rm -f $refpath
+		case uptodate;
+			echo $u(2)^': up to date'
+		}
+	}
+}
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/query.c
@@ -1,0 +1,196 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+#pragma	varargck	type	"P"	void
+
+int fullpath;
+int changes;
+int reverse;
+char *path[128];
+int npath;
+
+int
+Pfmt(Fmt *f)
+{
+	int i, n;
+
+	n = 0;
+	for(i = 0; i < npath; i++)
+		n += fmtprint(f, "%s/", path[i]);
+	return n;
+}
+
+void
+showdir(Hash dh, char *dname, char m)
+{
+	Dirent *p, *e;
+	Object *d;
+
+
+	path[npath++] = dname;
+	if((d = readobject(dh)) == nil)
+		sysfatal("bad hash %H", dh);
+	assert(d->type == GTree);
+	p = d->tree->ent;
+	e = p + d->tree->nent;
+	for(; p != e; p++){
+		if(p->ismod)
+			continue;
+		if(p->mode & DMDIR)
+			showdir(p->h, p->name, m);
+		else
+			print("%c %P%s\n", m, p->name);
+	}
+	print("%c %P\n", m);
+	unref(d);
+	npath--;
+}
+
+void
+show(Dirent *e, char m)
+{
+	if(e->mode & DMDIR)
+		showdir(e->h, e->name, m);
+	else
+		print("%c %P%s\n", m, e->name);
+}
+
+void
+difftrees(Object *a, Object *b)
+{
+	Dirent *ap, *bp, *ae, *be;
+	int c;
+
+	ap = ae = nil;
+	bp = be = nil;
+	if(a != nil){
+		if(a->type != GTree)
+			return;
+		ap = a->tree->ent;
+		ae = ap + a->tree->nent;
+	}
+	if(b != nil){
+		if(b->type != GTree)
+			return;
+		bp = b->tree->ent;
+		be = bp + b->tree->nent;
+	}
+	while(ap != ae && bp != be){
+		c = strcmp(ap->name, bp->name);
+		if(c == 0){
+			if(ap->mode == bp->mode && hasheq(&ap->h, &bp->h))
+				goto next;
+			if(ap->mode != bp->mode)
+				print("! %P%s\n", ap->name);
+			else if(!(ap->mode & DMDIR) || !(bp->mode & DMDIR))
+				print("@ %P%s\n", ap->name);
+			if((ap->mode & DMDIR) && (bp->mode & DMDIR)){
+				if(npath >= nelem(path))
+					sysfatal("path too deep");
+				path[npath++] = ap->name;
+				if((a = readobject(ap->h)) == nil)
+					sysfatal("bad hash %H", ap->h);
+				if((b = readobject(bp->h)) == nil)
+					sysfatal("bad hash %H", bp->h);
+				difftrees(a, b);
+				unref(a);
+				unref(b);
+				npath--;
+			}
+next:
+			ap++;
+			bp++;
+		}else if(c < 0) {
+			show(ap, '-');
+			ap++;
+		}else if(c > 0){
+			show(bp, '+');
+			bp++;
+		}
+	}
+	for(; ap != ae; ap++)
+		show(ap, '-');
+	for(; bp != be; bp++)
+		show(bp, '+');
+}
+
+void
+diffcommits(Hash ah, Hash bh)
+{
+	Object *a, *b, *at, *bt;
+
+	at = nil;
+	bt = nil;
+	if(!hasheq(&ah, &Zhash) && (a = readobject(ah)) != nil){
+		if(a->type != GCommit)
+			sysfatal("not commit: %H", ah);
+		if((at = readobject(a->commit->tree)) == nil)
+			sysfatal("bad hash %H", a->commit->tree);
+		unref(a);
+	}
+	if(!hasheq(&bh, &Zhash) && (b = readobject(bh)) != nil){
+		if(b->type != GCommit)
+			sysfatal("not commit: %H", ah);
+		if((bt = readobject(b->commit->tree)) == nil)
+			sysfatal("bad hash %H", b->commit->tree);
+		unref(b);
+	}
+	difftrees(at, bt);
+	unref(at);
+	unref(bt);
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-pcr] query\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	int i, j, n;
+	Hash *h;
+	char *p, *e, *s;
+	char query[2048], repo[512];
+
+	ARGBEGIN{
+	case 'p':	fullpath++;	break;
+	case 'c':	changes++;	break;
+	case 'r':	reverse ^= 1;	break;
+	default:	usage();	break;
+	}ARGEND;
+
+	gitinit();
+	fmtinstall('P', Pfmt);
+
+	if(argc == 0)
+		usage();
+	if(findrepo(repo, sizeof(repo)) == -1)
+		sysfatal("find root: %r");
+	if(chdir(repo) == -1)
+		sysfatal("chdir: %r");
+	s = "";
+	p = query;
+	e = query + nelem(query);
+	for(i = 0; i < argc; i++){
+		p = seprint(p, e, "%s%s", s, argv[i]);
+		s = " ";
+	}
+	if((n = resolverefs(&h, query)) == -1)
+		sysfatal("resolve: %r");
+	if(changes){
+		if(n != 2)
+			sysfatal("diff: need 2 commits, got %d", n);
+		diffcommits(h[0], h[1]);
+	}else{
+		p = (fullpath ? "/mnt/git/object/" : "");
+		for(j = 0; j < n; j++)
+			print("%s%H\n", p, h[reverse ? n - 1 - j : j]);
+	}
+	exits(nil);
+}
+
--- /dev/null
+++ b/sys/src/cmd/git/rebase
@@ -1,0 +1,92 @@
+#!/bin/rc
+
+. /sys/lib/git/common.rc
+gitup
+
+flagfmt='a:abort, r:resume, i:interactive'; args='onto'
+eval `''{aux/getflags $*} || exec aux/usage
+
+tmp=_rebase.working
+if(! git/walk -q)
+	die dirty working tree
+if(~ $#abort 1){
+	if(! test -f .git/rebase.todo)
+		die no rebase to abort
+	src=`{cat .git/rebase.src}
+	rm -f .git/rebase.^(src todo)
+	git/branch $src
+	git/branch -d $tmp
+	exit
+}
+if(test -f .git/rebase.todo){
+	if(~ $#resume 0)
+		die rebase in progress
+	if(! ~ $#* 0)
+		exec aux/usage
+	src=`{cat .git/rebase.src}
+}
+if not{
+	if(! ~ $#* 1)
+		exec aux/usage
+	src=`{git/branch}
+	dst=`{git/query $1}
+	echo $src > .git/rebase.src
+	git/log -se $dst' '$src' @ .. '$src | sed 's/^/pick /' >.git/rebase.todo
+	if(! ~ $#interactive 0){
+		giteditor=`{git/conf core.editor}
+		if(~ $#editor 0)
+			editor=$giteditor
+		if(~ $#editor 0)
+			editor=hold
+		$editor .git/rebase.todo
+	}
+	git/branch -nb $dst $tmp
+}
+todo=`$nl{cat .git/rebase.todo}
+
+fn sigexit {
+	s=$status
+	if(!)
+		echo 'fix and git/rebase -r'
+	>.git/rebase.todo for(i in $todo)
+		echo $i
+	status=$s
+}
+
+flag e +
+
+while(! ~ $#todo 0){
+	item=`{echo $todo(1)}
+	todo=$todo(2-)
+	echo $item
+	c=$item(2)
+	switch($item(1)){
+	case p pick
+		git/export $c | git/import
+	case r reword
+		git/export $c | git/import
+		git/commit -re
+	case e edit
+		git/export $c | git/import
+		echo 'stopped for edit, resume with git/rebase -r'
+		exit
+	case s squash
+		git/export $c | git/import -n
+		msg=`''{cat /mnt/git/HEAD/msg; echo; cat /mnt/git/object/$c/msg}
+		git/commit -rem $msg .
+	case f fixup
+		git/export $c | git/import -n
+		git/commit -r .
+	case b break
+		echo 'stopped, resume with git/rebase -r'
+		exit
+	case '#'* ''
+	case *
+		die 'unknown command '''^$item(1)^''''
+	}
+}
+
+fn sigexit
+git/branch -nb $tmp $src
+git/branch -d $tmp
+rm .git/rebase.todo .git/rebase.src
--- /dev/null
+++ b/sys/src/cmd/git/ref.c
@@ -1,0 +1,677 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+typedef struct Eval	Eval;
+typedef struct XObject	XObject;
+typedef struct Objq	Objq;
+
+enum {
+	Blank,
+	Keep,
+	Drop,
+};
+
+struct Eval {
+	char	*str;
+	char	*p;
+	Object	**stk;
+	int	nstk;
+	int	stksz;
+};
+
+struct XObject {
+	Object	*obj;
+	Object	*mark;
+	XObject	*queue;
+	XObject	*next;
+};
+
+struct Objq {
+	Objq	*next;
+	Object	*o;
+	int	color;
+};
+
+static Object zcommit = {
+	.type=GCommit
+};
+
+void
+eatspace(Eval *ev)
+{
+	while(isspace(ev->p[0]))
+		ev->p++;
+}
+
+int
+objdatecmp(void *pa, void *pb)
+{
+	Object *a, *b;
+	int r;
+
+	a = readobject((*(Object**)pa)->hash);
+	b = readobject((*(Object**)pb)->hash);
+	assert(a->type == GCommit && b->type == GCommit);
+	if(a->commit->mtime == b->commit->mtime)
+		r = 0;
+	else if(a->commit->mtime < b->commit->mtime)
+		r = -1;
+	else
+		r = 1;
+	unref(a);
+	unref(b);
+	return r;
+}
+
+void
+push(Eval *ev, Object *o)
+{
+	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
+isword(char e)
+{
+	return isalnum(e) || e == '/' || e == '-' || e == '_' || e == '.';
+}
+
+int
+word(Eval *ev, char *b, int nb)
+{
+	char *p, *e;
+	int n;
+
+	p = ev->p;
+	for(e = p; isword(*e) && strncmp(e, "..", 2) != 0; 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 = emalloc(sizeof(*h));
+	h->obj = o;
+	h->mark = nil;
+	h->queue = nil;
+	h->next = ht[hh];
+	ht[hh] = h;
+	return h;
+}
+
+Object*
+ancestor(Object *a, Object *b)
+{
+	Object *o, *p, *r;
+	XObject *ht[256];
+	XObject *h, *q, *q1, *q2;
+	int i;
+
+	if(a == b)
+		return a;
+	if(a == nil || b == nil)
+		return nil;
+	r = nil;
+	memset(ht, 0, sizeof(ht));
+	q1 = nil;
+
+	h = hnode(ht, a);
+	h->mark = a;
+	h->queue = q1;
+	q1 = h;
+
+	h = hnode(ht, b);
+	h->mark = b;
+	h->queue = q1;
+	q1 = h;
+
+	while(1){
+		q2 = nil;
+		while(q = q1){
+			q1 = q->queue;
+			q->queue = nil;
+			o = q->obj;
+			for(i = 0; i < o->commit->nparent; i++){
+				p = readobject(o->commit->parent[i]);
+				if(p == nil)
+					goto err;
+				h = hnode(ht, p);
+				if(h->mark != nil){
+					if(h->mark != q->mark){
+						r = h->obj;
+						goto done;
+					}
+				} else {
+					h->mark = q->mark;
+					h->queue = q2;
+					q2 = h;
+				}
+			}
+		}
+		if(q2 == nil){
+err:
+			werrstr("no common ancestor");
+			break;
+		}
+		q1 = q2;
+	}
+done:
+	for(i=0; i<nelem(ht); i++){
+		while(h = ht[i]){
+			ht[i] = h->next;
+			free(h);
+		}
+	}
+	return r;
+}
+
+int
+lca(Eval *ev)
+{
+	Object *a, *b, *o;
+
+	if(ev->nstk < 2){
+		werrstr("ancestor needs 2 objects");
+		return -1;
+	}
+	a = pop(ev);
+	b = pop(ev);
+	o = ancestor(a, b);
+	if(o == nil)
+		return -1;
+	push(ev, o);
+	return 0;
+}
+
+static int
+repaint(Objset *keep, Objset *drop, Object *o)
+{
+	Object *p;
+	int i;
+
+	if(!oshas(keep, o->hash) && !oshas(drop, o->hash)){
+		dprint(2, "repaint: blank => drop %H\n", o->hash);
+		osadd(drop, o);
+		return 0;
+	}
+	if(oshas(keep, o->hash))
+		dprint(2, "repaint: keep => drop %H\n", o->hash);
+	osadd(drop, o);
+	for(i = 0; i < o->commit->nparent; i++){
+		if((p = readobject(o->commit->parent[i])) == nil)
+			return -1;
+		if(repaint(keep, drop, p) == -1)
+			return -1;
+		unref(p);
+	}
+	return 0;
+}
+
+int
+findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres)
+{
+	Objq *q, *e, *n, **p;
+	Objset keep, drop;
+	Object *o, *c;
+	int i, ncolor;
+
+	e = nil;
+	q = nil;
+	p = &q;
+	osinit(&keep);
+	osinit(&drop);
+	for(i = 0; i < nhead; i++){
+		if(hasheq(&head[i], &Zhash))
+			continue;
+		if((o = readobject(head[i])) == nil){
+			fprint(2, "warning: %H does not point at commit\n", o->hash);
+			werrstr("read head %H: %r", head[i]);
+			return -1;
+		}
+		if(o->type != GCommit){
+			fprint(2, "warning: %H does not point at commit\n", o->hash);
+			unref(o);
+			continue;
+		}
+		dprint(1, "twixt init: keep %H\n", o->hash);
+		e = emalloc(sizeof(Objq));
+		e->o = o;
+		e->color = Keep;
+		*p = e;
+		p = &e->next;
+		unref(o);
+	}		
+	for(i = 0; i < ntail; i++){
+		if(hasheq(&tail[i], &Zhash))
+			continue;
+		if((o = readobject(tail[i])) == nil){
+			fprint(2, "warning: %H does not point at commit\n", o->hash);
+			werrstr("read tail %H: %r", tail[i]);
+			return -1;
+		}
+		if(o->type != GCommit){
+			unref(o);
+			continue;
+		}
+		dprint(1, "init: drop %H\n", o->hash);
+		e = emalloc(sizeof(Objq));
+		e->o = o;
+		e->color = Drop;
+		*p = e;
+		p = &e->next;
+		unref(o);
+	}
+
+	dprint(1, "finding twixt commits\n");
+	while(q != nil){
+		if(oshas(&drop, q->o->hash))
+			ncolor = Drop;
+		else if(oshas(&keep, q->o->hash))
+			ncolor = Keep;
+		else
+			ncolor = Blank;
+		if(ncolor == Drop || ncolor == Keep && q->color == Keep)
+			goto next;
+		if(ncolor == Keep && q->color == Drop){
+			if(repaint(&keep, &drop, q->o) == -1)
+				goto error;
+		}else if (ncolor == Blank) {
+			dprint(2, "visit: %s %H\n", q->color == Keep ? "keep" : "drop", q->o->hash);
+			if(q->color == Keep)
+				osadd(&keep, q->o);
+			else
+				osadd(&drop, q->o);
+			for(i = 0; i < q->o->commit->nparent; i++){
+				if((c = readobject(q->o->commit->parent[i])) == nil)
+					goto error;
+				if(c->type != GCommit){
+					fprint(2, "warning: %H does not point at commit\n", c->hash);
+					unref(c);
+					continue;
+				}
+				dprint(2, "enqueue: %s %H\n", q->color == Keep ? "keep" : "drop", c->hash);
+				n = emalloc(sizeof(Objq));
+				n->color = q->color;
+				n->next = nil;
+				n->o = c;
+				e->next = n;
+				e = n;
+				unref(c);
+			}
+		}else{
+			sysfatal("oops");
+		}
+next:
+		n = q->next;
+		free(q);
+		q = n;
+	}
+	*res = eamalloc(keep.nobj, sizeof(Object*));
+	*nres = 0;
+	for(i = 0; i < keep.sz; i++){
+		if(keep.obj[i] != nil && !oshas(&drop, keep.obj[i]->hash)){
+			(*res)[*nres] = keep.obj[i];
+			(*nres)++;
+		}
+	}
+	osclear(&keep);
+	osclear(&drop);
+	return 0;
+error:
+	for(; q != nil; q = n) {
+		n = q->next;
+		free(q);
+	}
+	return -1;
+}
+
+static int
+parent(Eval *ev)
+{
+	Object *o, *p;
+
+	o = pop(ev);
+	/* Special case: first commit has no parent. */
+	if(o->commit->nparent == 0)
+		p = emptydir();
+	else if ((p = readobject(o->commit->parent[0])) == nil){
+		werrstr("no parent for %H", o->hash);
+		return -1;
+	}
+		
+	push(ev, p);
+	return 0;
+}
+
+static int
+unwind(Eval *ev, Object **obj, int *idx, int nobj, Object **p, Objset *set, int keep)
+{
+	int i;
+
+	for(i = nobj; i >= 0; i--){
+		idx[i]++;
+		if(keep && !oshas(set, obj[i]->hash)){
+			push(ev, obj[i]);
+			osadd(set, obj[i]);
+		}else{
+			osadd(set, obj[i]);
+		}
+		if(idx[i] < obj[i]->commit->nparent){
+			*p = obj[i];
+			return i;
+		}
+		unref(obj[i]);
+	}
+	return -1;
+}
+
+static int
+range(Eval *ev)
+{
+	Object *a, *b, *p, *q, **all;
+	int nall, *idx, mark;
+	Objset keep, skip;
+
+	b = pop(ev);
+	a = pop(ev);
+	if(hasheq(&b->hash, &Zhash))
+		b = &zcommit;
+	if(hasheq(&a->hash, &Zhash))
+		a = &zcommit;
+	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);
+	osadd(&keep, a);
+	while(1){
+		all = earealloc(all, (nall + 1), sizeof(Object*));
+		idx = earealloc(idx, (nall + 1), sizeof(int));
+		all[nall] = p;
+		idx[nall] = 0;
+		if(p == a || p->commit->nparent == 0 && a == &zcommit){
+			if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1)
+				break;
+		}else if(p->commit->nparent == 0){
+			if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1)
+				break;
+		}else if(oshas(&keep, p->hash)){
+			if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1)
+				break;
+		}else if(oshas(&skip, p->hash))
+			if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1)
+				break;
+		if(p->commit->nparent == 0)
+			break;
+		if((q = readobject(p->commit->parent[idx[nall]])) == nil){
+			werrstr("bad commit %H", p->commit->parent[idx[nall]]);
+			goto error;
+		}
+		if(q->type != GCommit){
+			werrstr("not commit: %H", q->hash);
+			goto error;
+		}
+		p = q;
+		nall++;
+	}
+	free(all);
+	qsort(ev->stk + mark, ev->nstk - mark, sizeof(Object*), objdatecmp);
+	return 0;
+error:
+	free(all);
+	return -1;
+}
+
+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("invalid ref %s", name);
+		return -1;
+	}
+	if(hasheq(&h, &Zhash))
+		o = &zcommit;
+	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 '^':
+		case '~':
+			ev->p++;
+			if(parent(ev) == -1)
+				return -1;
+			break;
+		case '@':
+			ev->p++;
+			if(lca(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 = eamalloc(ev.nstk, sizeof(Hash));
+	for(i = 0; i < ev.nstk; i++)
+		h[i] = ev.stk[i]->hash;
+	*r = h;
+	free(ev.stk);
+	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;
+	free(ev.stk);
+	return 0;
+}
+
+int
+readrefdir(Hash **refs, char ***names, int *nrefs, char *dpath, char *dname)
+{
+	Dir *d, *e, *dir;
+	char *path, *name, *sep;
+	int ndir;
+
+	if((ndir = slurpdir(dpath, &dir)) == -1)
+		return -1;
+	sep = (*dname == '\0') ? "" : "/";
+	e = dir + ndir;
+	for(d = dir; d != e; d++){
+		path = smprint("%s/%s", dpath, d->name);
+		name = smprint("%s%s%s", dname, sep, d->name);
+		if(d->mode & DMDIR) {
+			if(readrefdir(refs, names, nrefs, path, name) == -1)
+				goto noref;
+		}else{
+			*refs = erealloc(*refs, (*nrefs + 1)*sizeof(Hash));
+			*names = erealloc(*names, (*nrefs + 1)*sizeof(char*));
+			if(resolveref(&(*refs)[*nrefs], name) == -1)
+				goto noref;
+			(*names)[*nrefs] = name;
+			*nrefs += 1;
+			goto next;
+		}
+noref:		free(name);
+next:		free(path);
+	}
+	free(dir);
+	return 0;
+}
+
+int
+listrefs(Hash **refs, char ***names)
+{
+	int nrefs;
+
+	*refs = nil;
+	*names = nil;
+	nrefs = 0;
+	if(readrefdir(refs, names, &nrefs, ".git/refs", "") == -1){
+		free(*refs);
+		return -1;
+	}
+	return nrefs;
+}
--- /dev/null
+++ b/sys/src/cmd/git/repack.c
@@ -1,0 +1,85 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+#define TMPPATH(suff) (".git/objects/pack/repack."suff)
+
+int
+cleanup(Hash h)
+{
+	char newpfx[42], dpath[256], fpath[256];
+	int i, j, nd;
+	Dir *d;
+
+	snprint(newpfx, sizeof(newpfx), "%H.", h);
+	for(i = 0; i < 256; i++){
+		snprint(dpath, sizeof(dpath), ".git/objects/%02x", i);
+		if((nd = slurpdir(dpath, &d)) == -1)
+			continue;
+		for(j = 0; j < nd; j++){
+			snprint(fpath, sizeof(fpath), ".git/objects/%02x/%s", i, d[j].name);
+			remove(fpath);
+		}
+		remove(dpath);
+		free(d);
+	}
+	snprint(dpath, sizeof(dpath), ".git/objects/pack");
+	if((nd = slurpdir(dpath, &d)) == -1)
+		return -1;
+	for(i = 0; i < nd; i++){
+		if(strncmp(d[i].name, newpfx, strlen(newpfx)) == 0)
+			continue;
+		snprint(fpath, sizeof(fpath), ".git/objects/pack/%s", d[i].name);
+		remove(fpath);
+	}
+	return 0;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-d]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char path[128], **names;
+	int fd, nrefs;
+	Hash *refs, h;
+	Dir rn;
+
+	ARGBEGIN{
+	case 'd':
+		chattygit++;
+		break;
+	default:
+		usage();
+	}ARGEND;
+
+	gitinit();
+	refs = nil;
+	if((nrefs = listrefs(&refs, &names)) == -1)
+		sysfatal("load refs: %r");
+	if((fd = create(TMPPATH("pack.tmp"), OWRITE, 0644)) == -1)
+		sysfatal("open %s: %r", TMPPATH("pack.tmp"));
+	if(writepack(fd, refs, nrefs, nil, 0, &h) == -1)
+		sysfatal("writepack: %r");
+	if(indexpack(TMPPATH("pack.tmp"), TMPPATH("idx.tmp"), h) == -1)
+		sysfatal("indexpack: %r");
+	close(fd);
+
+	nulldir(&rn);
+	rn.name = path;
+	snprint(path, sizeof(path), "%H.pack", h);
+	if(dirwstat(TMPPATH("pack.tmp"), &rn) == -1)
+		sysfatal("rename pack: %r");
+	snprint(path, sizeof(path), "%H.idx", h);
+	if(dirwstat(TMPPATH("idx.tmp"), &rn) == -1)
+		sysfatal("rename pack: %r");
+	if(cleanup(h) == -1)
+		sysfatal("cleanup: %r");
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/git/revert
@@ -1,0 +1,19 @@
+#!/bin/rc
+rfork e
+. /sys/lib/git/common.rc
+
+gitup
+
+flagfmt='c:query query' args='file ...'
+eval `''{aux/getflags $*} || exec aux/usage
+
+commit=/mnt/git/HEAD
+if(~ $#query 1)
+	commit=`{git/query -p $query}
+
+for(f in `$nl{cd $commit/tree/ && walk -f ./$gitrel/$*}){
+	mkdir -p `{basename -d $f}
+	cp -- $commit/tree/$f $f
+	git/add $f
+}
+exit ''
--- /dev/null
+++ b/sys/src/cmd/git/rm
@@ -1,0 +1,3 @@
+#!/bin/rc -e
+
+exec git/add -r $*
--- /dev/null
+++ b/sys/src/cmd/git/save.c
@@ -1,0 +1,401 @@
+#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,
+};
+
+int
+gitmode(int m)
+{
+	if(m & DMDIR)		/* directory */
+		return 0040000;
+	else if(m & 0111)	/* executable */
+		return 0100755;
+	else if(m != 0)		/* regular */
+		return 0100644;
+	else			/* symlink */
+		return 0120000;
+}
+
+int
+entcmp(void *pa, void *pb)
+{
+	char abuf[256], bbuf[256], *ae, *be;
+	Dirent *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)
+		return 0;
+
+	ae = seprint(abuf, abuf + sizeof(abuf) - 1, a->name);
+	be = seprint(bbuf, bbuf + sizeof(bbuf) - 1, b->name);
+	if(a->mode & DMDIR)
+		*ae = '/';
+	if(b->mode & DMDIR)
+		*be = '/';
+	return strcmp(abuf, bbuf);
+}
+
+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
+writetree(Dirent *ent, int nent, Hash *h)
+{
+	char *t, *txt, *etxt, hdr[128];
+	int nhdr, n;
+	Dirent *d, *p;
+
+	t = emalloc((16+256+20) * nent);
+	txt = t;
+	etxt = t + (16+256+20) * nent;
+
+	/* sqeeze out deleted entries */
+	n = 0;
+	p = ent;
+	for(d = ent; d != ent + nent; d++)
+		if(d->name)
+			p[n++] = *d;
+	nent = n;
+
+	qsort(ent, nent, sizeof(Dirent), entcmp);
+	for(d = ent; d != ent + nent; d++){
+		if(strlen(d->name) >= 255)
+			sysfatal("overly long filename: %s", d->name);
+		t = seprint(t, etxt, "%o %s", gitmode(d->mode), d->name) + 1;
+		memcpy(t, d->h.h, sizeof(d->h.h));
+		t += sizeof(d->h.h);
+	}
+	nhdr = snprint(hdr, sizeof(hdr), "%T %lld", GTree, (vlong)(t - txt)) + 1;
+	writeobj(h, hdr, nhdr, txt, t - txt);
+	free(txt);
+	return nent;
+}
+
+void
+blobify(Dir *d, char *path, int *mode, Hash *bh)
+{
+	char h[64], *buf;
+	int f, nh;
+
+	if((d->mode & DMDIR) != 0)
+		sysfatal("not file: %s", path);
+	*mode = d->mode;
+	nh = snprint(h, sizeof(h), "%T %lld", GBlob, d->length) + 1;
+	if((f = open(path, OREAD)) == -1)
+		sysfatal("could not open %s: %r", path);
+	buf = emalloc(d->length);
+	if(readn(f, buf, d->length) != d->length)
+		sysfatal("could not read blob %s: %r", path);
+	writeobj(bh, h, nh, buf, d->length);
+	free(buf);
+	close(f);
+}
+
+int
+tracked(char *path)
+{
+	char ipath[256];
+	Dir *d;
+
+	/* 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
+pathelt(char *buf, int nbuf, char *p, int *isdir)
+{
+	char *b;
+
+	b = buf;
+	if(*p == '/')
+		p++;
+	while(*p && *p != '/' && b != buf + nbuf)
+		*b++ = *p++;
+	*b = '\0';
+	*isdir = (*p == '/');
+	return b - buf;
+}
+
+Dirent*
+dirent(Dirent **ent, int *nent, char *name)
+{
+	Dirent *d;
+
+	for(d = *ent; d != *ent + *nent; d++)
+		if(d->name && strcmp(d->name, name) == 0)
+			return d;
+	*nent += 1;
+	*ent = erealloc(*ent, *nent * sizeof(Dirent));
+	d = *ent + (*nent - 1);
+	memset(d, 0, sizeof(*d));
+	d->name = estrdup(name);
+	return d;
+}
+
+int
+treeify(Object *t, char **path, char **epath, int off, Hash *h)
+{
+	int r, n, ne, nsub, nent, isdir;
+	char **p, **ep;
+	char elt[256];
+	Object **sub;
+	Dirent *e, *ent;
+	Dir *d;
+
+	r = -1;
+	nsub = 0;
+	nent = t->tree->nent;
+	ent = eamalloc(nent, sizeof(*ent));
+	sub = eamalloc((epath - path), sizeof(Object*));
+	memcpy(ent, t->tree->ent, nent*sizeof(*ent));
+	for(p = path; p != epath; p = ep){
+		ne = pathelt(elt, sizeof(elt), *p + off, &isdir);
+		for(ep = p; ep != epath; ep++){
+			if(strncmp(elt, *ep + off, ne) != 0)
+				break;
+			if((*ep)[off+ne] != '\0' && (*ep)[off+ne] != '/')
+				break;
+		}
+		e = dirent(&ent, &nent, elt);
+		if(e->islink)
+			sysfatal("symlinks may not be modified: %s", *path);
+		if(e->ismod)
+			sysfatal("submodules may not be modified: %s", *path);
+		if(isdir){
+			e->mode = DMDIR | 0755;
+			sub[nsub] = readobject(e->h);
+			if(sub[nsub] == nil || sub[nsub]->type != GTree)
+				sub[nsub] = emptydir();
+			/*
+			 * if after processing deletions, a tree is empty,
+			 * mark it for removal from the parent.
+			 *
+			 * Note, it is still written to the object store,
+			 * but this is fine -- and ensures that an empty
+			 * repository will continue to work.
+			 */
+			n = treeify(sub[nsub], p, ep, off + ne + 1, &e->h);
+			if(n == 0)
+				e->name = nil;
+			else if(n == -1)
+				goto err;
+		}else{
+			d = dirstat(*p);
+			if(d != nil && tracked(*p))
+				blobify(d, *p, &e->mode, &e->h);
+			else
+				e->name = nil;
+			free(d);
+		}
+	}
+	if(nent == 0){
+		werrstr("%.*s: empty directory", off, *path);
+		goto err;
+	}
+
+	r = writetree(ent, nent, h);
+err:
+	free(sub);
+	return r;		
+}
+
+
+void
+mkcommit(Hash *c, char *msg, char *name, char *email, vlong date, 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, date);
+	fmtprint(&f, "committer %s <%s> %lld +0000\n", name, email, date);
+	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);
+}
+
+Object*
+findroot(void)
+{
+	Object *t, *c;
+	Hash h;
+
+	if(resolveref(&h, "HEAD") == -1)
+		return emptydir();
+	if((c = readobject(h)) == nil || c->type != GCommit)
+		sysfatal("could not read HEAD %H", h);
+	if((t = readobject(c->commit->tree)) == nil)
+		sysfatal("could not read tree for commit %H", h);
+	return t;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s -n name -e email -m message -d date [files...]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	Hash th, ch, parents[Maxparents];
+	char *msg, *name, *email, *dstr;
+	int i, r, nparents;
+	vlong date;
+	Object *t;
+
+	msg = nil;
+	name = nil;
+	email = nil;
+	dstr = nil;
+	date = time(nil);
+	nparents = 0;
+	gitinit();
+	ARGBEGIN{
+	case 'm':	msg = EARGF(usage());	break;
+	case 'n':	name = EARGF(usage());	break;
+	case 'e':	email = EARGF(usage());	break;
+	case 'd':	dstr = EARGF(usage());	break;
+	case 'p':
+		if(nparents >= Maxparents)
+			sysfatal("too many parents");
+		if(resolveref(&parents[nparents++], EARGF(usage())) == -1)
+			sysfatal("invalid parent: %r");
+		break;
+	default:
+		usage();
+	}ARGEND;
+
+	if(!msg)
+		sysfatal("missing message");
+	if(!name)
+		sysfatal("missing name");
+	if(!email)
+		sysfatal("missing email");
+	if(dstr){
+		date=strtoll(dstr, &dstr, 10);
+		if(strlen(dstr) != 0)
+			sysfatal("could not parse date %s", dstr);
+	}
+	if(msg == nil || name == nil)
+		usage();
+	for(i = 0; i < argc; i++)
+		cleanname(argv[i]);
+
+	gitinit();
+	if(access(".git", AEXIST) != 0)
+		sysfatal("could not find git repo: %r");
+	t = findroot();
+	r = treeify(t, argv, argv + argc, 0, &th);
+	if(r == -1)
+		sysfatal("could not commit: %r\n");
+	mkcommit(&ch, msg, name, email, date, parents, nparents, th);
+	print("%H\n", ch);
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/git/send.c
@@ -1,0 +1,272 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+typedef struct Capset	Capset;
+
+struct Capset {
+	int	sideband;
+	int	sideband64k;
+	int	report;
+};
+
+int sendall;
+int force;
+int nbranch;
+char **branch;
+char *removed[128];
+int nremoved;
+int npacked;
+int nsent;
+
+int
+findref(char **r, int nr, char *ref)
+{
+	int i;
+
+	for(i = 0; i < nr; i++)
+		if(strcmp(r[i], ref) == 0)
+			return i;
+	return -1;
+}
+
+int
+readours(Hash **tailp, char ***refp)
+{
+	int nu, i, idx;
+	char *r, *pfx, **ref;
+	Hash *tail;
+
+	if(sendall)
+		return listrefs(tailp, refp);
+	nu = 0;
+	tail = eamalloc((nremoved + nbranch), sizeof(Hash));
+	ref = eamalloc((nremoved + nbranch), sizeof(char*));
+	for(i = 0; i < nbranch; i++){
+		ref[nu] = estrdup(branch[i]);
+		if(resolveref(&tail[nu], branch[i]) == -1)
+			sysfatal("broken branch %s", branch[i]);
+		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 = "";
+		if((r = smprint("%s%s", pfx, removed[i])) == nil)
+			sysfatal("smprint: %r");
+		if((idx = findref(ref, nu, r)) == -1)
+			idx = nu++;
+		assert(idx < nremoved + nbranch);
+		memcpy(&tail[idx], &Zhash, sizeof(Hash));
+		free(r);
+	}
+	dprint(1, "nu: %d\n", nu);
+	for(i = 0; i < nu; i++)
+		dprint(1, "update: %H %s\n", tail[i], ref[i]);
+	*tailp = tail;
+	*refp = ref;
+	return nu;	
+}
+
+char *
+matchcap(char *s, char *cap, int full)
+{
+	if(strncmp(s, cap, strlen(cap)) == 0)
+		if(!full || strlen(s) == strlen(cap))
+			return s + strlen(cap);
+	return nil;
+}
+
+void
+parsecaps(char *caps, Capset *cs)
+{
+	char *p, *n;
+
+	for(p = caps; p != nil; p = n){
+		n = strchr(p, ' ');
+		if(n != nil)
+			*n++ = 0;
+		if(matchcap(p, "report-status", 1) != nil)
+			cs->report = 1;
+		if(matchcap(p, "side-band", 1) != nil)
+			cs->sideband = 1;
+		if(matchcap(p, "side-band-64k", 1) != nil)
+			cs->sideband64k = 1;
+	}
+}
+
+int
+sendpack(Conn *c)
+{
+	int i, n, idx, nupd, nsp, send, first;
+	char buf[Pktmax], *sp[3];
+	Hash h, *theirs, *ours;
+	Object *a, *b, *p;
+	char **refs;
+	Capset cs;
+
+	first = 1;
+	memset(&cs, 0, sizeof(Capset));
+	nupd = readours(&ours, &refs);
+	theirs = eamalloc(nupd, sizeof(Hash));
+	while(1){
+		n = readpkt(c, buf, sizeof(buf));
+		if(n == -1)
+			return -1;
+		if(n == 0)
+			break;
+		if(first && n > strlen(buf))
+			parsecaps(buf + strlen(buf) + 1, &cs);
+		first = 0;
+		if(strncmp(buf, "ERR ", 4) == 0)
+			sysfatal("%s", buf + 4);
+
+		if(getfields(buf, sp, nelem(sp), 1, " \t\r\n") != 2)
+			sysfatal("invalid ref line %.*s", utfnlen(buf, n), buf);
+		if((idx = findref(refs, nupd, sp[1])) == -1)
+			continue;
+		if(hparse(&theirs[idx], sp[0]) == -1)
+			sysfatal("invalid hash %s", sp[0]);
+	}
+
+	if(writephase(c) == -1)
+		return -1;
+	send = 0;
+	if(force)
+		send=1;
+	for(i = 0; i < nupd; i++){
+		a = readobject(theirs[i]);
+		b = hasheq(&ours[i], &Zhash) ? nil : readobject(ours[i]);
+		p = nil;
+		if(a != nil && b != nil)
+			p = ancestor(a, b);
+		if(!force && !hasheq(&theirs[i], &Zhash) && (a == nil || p != a)){
+			fprint(2, "remote has diverged\n");
+			werrstr("force needed");
+			flushpkt(c);
+			return -1;
+		}
+		unref(a);
+		unref(b);
+		unref(p);
+		if(hasheq(&theirs[i], &ours[i])){
+			print("uptodate %s\n", refs[i]);
+			continue;
+		}
+		print("update %s %H %H\n", refs[i], theirs[i], ours[i]);
+		n = snprint(buf, sizeof(buf), "%H %H %s", theirs[i], ours[i], refs[i]);
+
+		/*
+		 * 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 && cs.report){
+			buf[n++] = '\0';
+			n += snprint(buf + n, sizeof(buf) - n, " report-status");
+		}
+		if(writepkt(c, buf, n) == -1)
+			sysfatal("unable to send update pkt");
+		send = 1;
+	}
+	flushpkt(c);
+	if(!send){
+		fprint(2, "nothing to send\n");
+		return 0;
+	}
+
+	if(writepack(c->wfd, ours, nupd, theirs, nupd, &h) == -1)
+		return -1;
+	if(!cs.report)
+		return 0;
+
+	if(readphase(c) == -1)
+		return -1;
+	/* We asked for a status report, may as well use it. */
+	while((n = readpkt(c, buf, sizeof(buf))) > 0){
+ 		buf[n] = 0;
+		if(chattygit)
+			fprint(2, "done sending pack, status %s\n", buf);
+		nsp = getfields(buf, sp, nelem(sp), 1, " \t\n\r");
+		if(nsp < 2) 
+			continue;
+		if(nsp < 3)
+			sp[2] = "";
+		/*
+		 * Only report errors; successes will be reported by
+		 * surrounding scripts.
+		 */
+		if(strcmp(sp[0], "unpack") == 0 && strcmp(sp[1], "ok") != 0)
+			fprint(2, "unpack %s\n", sp[1]);
+		else if(strcmp(sp[0], "ng") == 0)
+			fprint(2, "failed update: %s\n", sp[1]);
+		else
+			continue;
+		return -1;
+	}
+	return 0;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s remote [reponame]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *br;
+	Conn c;
+
+	ARGBEGIN{
+	default:
+		usage();
+		break;
+	case 'd':
+		chattygit++;
+		break;
+	case 'f':
+		force++;
+		break;
+	case 'r':
+		if(nremoved == nelem(removed))
+			sysfatal("too many deleted branches");
+		removed[nremoved++] = EARGF(usage());
+		break;
+	case 'a':
+		sendall++;
+		break;
+	case 'b':
+		br = EARGF(usage());
+		if(strncmp(br, "refs/heads/", strlen("refs/heads/")) == 0)
+			br = smprint("%s", br);
+		else if(strncmp(br, "heads/", strlen("heads/")) == 0)
+			br = smprint("refs/%s", br);
+		else
+			br = smprint("refs/heads/%s", br);
+		branch = erealloc(branch, (nbranch + 1)*sizeof(char*));
+		branch[nbranch] = br;
+		nbranch++;
+		break;
+	}ARGEND;
+
+	gitinit();
+	if(argc != 1)
+		usage();
+	if(gitconnect(&c, argv[0], "receive") == -1)
+		sysfatal("git connect: %s: %r", argv[0]);
+	if(sendpack(&c) == -1)
+		sysfatal("send failed: %r");
+	closeconn(&c);
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/git/serve.c
@@ -1,0 +1,558 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <auth.h>
+
+#include "git.h"
+
+char	*pathpfx = "/usr/git";
+char	*namespace = nil;
+int	allowwrite;
+
+int
+fmtpkt(Conn *c, char *fmt, ...)
+{
+	char pkt[Pktmax];
+	va_list ap;
+	int n;
+
+	va_start(ap, fmt);
+	n = vsnprint(pkt, sizeof(pkt), fmt, ap);
+	n = writepkt(c, pkt, n);
+	va_end(ap);
+	return n;
+}
+
+int
+showrefs(Conn *c)
+{
+	int i, ret, nrefs;
+	Hash head, *refs;
+	char **names;
+
+	ret = -1;
+	nrefs = 0;
+	refs = nil;
+	names = nil;
+	if(resolveref(&head, "HEAD") != -1)
+		if(fmtpkt(c, "%H HEAD", head) == -1)
+			goto error;
+
+	if((nrefs = listrefs(&refs, &names)) == -1)
+		sysfatal("listrefs: %r");
+	for(i = 0; i < nrefs; i++){
+		if(strncmp(names[i], "heads/", strlen("heads/")) != 0)
+			continue;
+		if(fmtpkt(c, "%H refs/%s\n", refs[i], names[i]) == -1)
+			goto error;
+	}
+	if(flushpkt(c) == -1)
+		goto error;
+	ret = 0;
+error:
+	for(i = 0; i < nrefs; i++)
+		free(names[i]);
+	free(names);
+	free(refs);
+	return ret;
+}
+
+int
+servnegotiate(Conn *c, Hash **head, int *nhead, Hash **tail, int *ntail)
+{
+	char pkt[Pktmax];
+	int n, acked;
+	Object *o;
+	Hash h;
+
+	if(showrefs(c) == -1)
+		return -1;
+
+	*head = nil;
+	*tail = nil;
+	*nhead = 0;
+	*ntail = 0;
+	while(1){
+		if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
+			goto error;
+		if(n == 0)
+			break;
+		if(strncmp(pkt, "want ", 5) != 0){
+			werrstr(" protocol garble %s", pkt);
+			goto error;
+		}
+		if(hparse(&h, &pkt[5]) == -1){
+			werrstr(" garbled want");
+			goto error;
+		}
+		if((o = readobject(h)) == nil){
+			werrstr("requested nonexistent object");
+			goto error;
+		}
+		unref(o);
+		*head = erealloc(*head, (*nhead + 1)*sizeof(Hash));
+		(*head)[*nhead] = h;	
+		*nhead += 1;
+	}
+
+	acked = 0;
+	while(1){
+		if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
+			goto error;
+		if(strncmp(pkt, "done", 4) == 0)
+			break;
+		if(n == 0){
+			if(!acked && fmtpkt(c, "NAK") == -1)
+					goto error;
+		}
+		if(strncmp(pkt, "have ", 5) != 0){
+			werrstr(" protocol garble %s", pkt);
+			goto error;
+		}
+		if(hparse(&h, &pkt[5]) == -1){
+			werrstr(" garbled have");
+			goto error;
+		}
+		if((o = readobject(h)) == nil)
+			continue;
+		if(!acked){
+			if(fmtpkt(c, "ACK %H", h) == -1)
+				goto error;
+			acked = 1;
+		}
+		unref(o);
+		*tail = erealloc(*tail, (*ntail + 1)*sizeof(Hash));
+		(*tail)[*ntail] = h;	
+		*ntail += 1;
+	}
+	if(!acked && fmtpkt(c, "NAK\n") == -1)
+		goto error;
+	return 0;
+error:
+	fmtpkt(c, "ERR %r\n");
+	free(*head);
+	free(*tail);
+	return -1;
+}
+
+int
+servpack(Conn *c)
+{
+	Hash *head, *tail, h;
+	int nhead, ntail;
+
+	dprint(1, "negotiating pack\n");
+	if(servnegotiate(c, &head, &nhead, &tail, &ntail) == -1)
+		sysfatal("negotiate: %r");
+	dprint(1, "writing pack\n");
+	if(writepack(c->wfd, head, nhead, tail, ntail, &h) == -1)
+		sysfatal("send: %r");
+	return 0;
+}
+
+int
+validref(char *s)
+{
+	if(strncmp(s, "refs/", 5) != 0)
+		return 0;
+	for(; *s != '\0'; s++)
+		if(!isalnum(*s) && strchr("/-_.", *s) == nil)
+			return 0;
+	return 1;
+}
+
+int
+recvnegotiate(Conn *c, Hash **cur, Hash **upd, char ***ref, int *nupd)
+{
+	char pkt[Pktmax], *sp[4];
+	Hash old, new;
+	int n, i;
+
+	if(showrefs(c) == -1)
+		return -1;
+	*cur = nil;
+	*upd = nil;
+	*ref = nil;
+	*nupd = 0;
+	while(1){
+		if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
+			goto error;
+		if(n == 0)
+			break;
+		if(getfields(pkt, sp, nelem(sp), 1, " \t\n\r") != 3){
+			fmtpkt(c, "ERR  protocol garble %s\n", pkt);
+			goto error;
+		}
+		if(hparse(&old, sp[0]) == -1){
+			fmtpkt(c, "ERR bad old hash %s\n", sp[0]);
+			goto error;
+		}
+		if(hparse(&new, sp[1]) == -1){
+			fmtpkt(c, "ERR bad new hash %s\n", sp[1]);
+			goto error;
+		}
+		if(!validref(sp[2])){
+			fmtpkt(c, "ERR invalid ref %s\n", sp[2]);
+			goto error;
+		}
+		*cur = erealloc(*cur, (*nupd + 1)*sizeof(Hash));
+		*upd = erealloc(*upd, (*nupd + 1)*sizeof(Hash));
+		*ref = erealloc(*ref, (*nupd + 1)*sizeof(Hash));
+		(*cur)[*nupd] = old;
+		(*upd)[*nupd] = new;
+		(*ref)[*nupd] = estrdup(sp[2]);
+		*nupd += 1;
+	}		
+	return 0;
+error:
+	free(*cur);
+	free(*upd);
+	for(i = 0; i < *nupd; i++)
+		free((*ref)[i]);
+	free(*ref);
+	return -1;
+}
+
+int
+rename(char *pack, char *idx, Hash h)
+{
+	char name[128], path[196];
+	Dir st;
+
+	nulldir(&st);
+	st.name = name;
+	snprint(name, sizeof(name), "%H.pack", h);
+	snprint(path, sizeof(path), ".git/objects/pack/%s", name);
+	if(access(path, AEXIST) == 0)
+		fprint(2, "warning, pack %s already pushed\n", name);
+	else if(dirwstat(pack, &st) == -1)
+		return -1;
+	snprint(name, sizeof(name), "%H.idx", h);
+	snprint(path, sizeof(path), ".git/objects/pack/%s", name);
+	if(access(path, 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[Pktmax];
+	vlong n, r;
+	int nr;
+	
+	if(sz < 28){
+		werrstr("undersize packfile");
+		return -1;
+	}
+
+	st = nil;
+	n = 0;
+	if(seek(fd, 0, 0) == -1)
+		sysfatal("packfile seek: %r");
+	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){
+			werrstr("short read");
+			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
+mkdir(char *dir)
+{
+	char buf[ERRMAX];
+	int f;
+
+	if(access(dir, AEXIST) == 0)
+		return 0;
+	if((f = create(dir, OREAD, DMDIR | 0755)) == -1){
+		rerrstr(buf, sizeof(buf));
+		if(strstr(buf, "exist") == nil)
+			return -1;
+	}
+	close(f);
+	return 0;
+}
+
+int
+updatepack(Conn *c)
+{
+	char buf[Pktmax], packtmp[128], idxtmp[128], ebuf[ERRMAX];
+	int n, pfd, packsz;
+	Hash h;
+
+	/* make sure the needed dirs exist */
+	if(mkdir(".git/objects") == -1)
+		return -1;
+	if(mkdir(".git/objects/pack") == -1)
+		return -1;
+	if(mkdir(".git/refs") == -1)
+		return -1;
+	if(mkdir(".git/refs/heads") == -1)
+		return -1;
+	snprint(packtmp, sizeof(packtmp), ".git/objects/pack/recv-%d.pack.tmp", getpid());
+	snprint(idxtmp, sizeof(idxtmp), ".git/objects/pack/recv-%d.idx.tmp", getpid());
+	if((pfd = create(packtmp, ORDWR, 0644)) == -1)
+		return -1;
+	packsz = 0;
+	while(1){
+		n = read(c->rfd, buf, sizeof(buf));
+		if(n == 0)
+			break;
+		if(n == -1){
+			rerrstr(ebuf, sizeof(ebuf));
+			if(strstr(ebuf, "hungup") == nil)
+				return -1;
+			break;
+		}
+		if(write(pfd, buf, n) != n)
+			return -1;
+		packsz += n;
+	}
+	if(checkhash(pfd, packsz, &h) == -1){
+		dprint(1, "hash mismatch\n");
+		goto error1;
+	}
+	if(indexpack(packtmp, idxtmp, h) == -1){
+		dprint(1, "indexing failed: %r\n");
+		goto error1;
+	}
+	if(rename(packtmp, idxtmp, h) == -1){
+		dprint(1, "rename failed: %r\n");
+		goto error2;
+	}
+	return 0;
+
+error2:	remove(idxtmp);
+error1:	remove(packtmp);
+	return -1;
+}	
+
+int
+lockrepo(void)
+{
+	int fd, i;
+
+	for(i = 0; i < 10; i++) {
+		if((fd = create(".git/_lock", ORCLOSE|ORDWR|OTRUNC|OEXCL, 0644))!= -1)
+			return fd;
+		sleep(250);
+	}
+	return -1;
+}
+
+int
+updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd)
+{
+	char refpath[512];
+	int i, newidx, hadref, fd, ret, lockfd;
+	vlong newtm;
+	Object *o;
+	Hash h;
+
+	ret = -1;
+	hadref = 0;
+	newidx = -1;
+	/*
+	 * Date of Magna Carta.
+	 * Wrong because it  was computed using
+	 * the proleptic gregorian calendar.
+	 */
+	newtm = -23811206400;	
+	if((lockfd = lockrepo()) == -1){
+		werrstr("repo locked\n");
+		return -1;
+	}
+	for(i = 0; i < nupd; i++){
+		if(resolveref(&h, ref[i]) == 0){
+			hadref = 1;
+			if(!hasheq(&h, &cur[i])){
+				werrstr("old ref changed: %s", ref[i]);
+				goto error;
+			}
+		}
+		if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){
+			werrstr("ref path too long: %s", ref[i]);
+			goto error;
+		}
+		if(hasheq(&upd[i], &Zhash)){
+			remove(refpath);
+			continue;
+		}
+		if((o = readobject(upd[i])) == nil){
+			werrstr("update to nonexistent hash %H", upd[i]);
+			goto error;
+		}
+		if(o->type != GCommit){
+			werrstr("not commit: %H", upd[i]);
+			goto error;
+		}
+		if(o->commit->mtime > newtm){
+			newtm = o->commit->mtime;
+			newidx = i;
+		}
+		unref(o);
+		if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){
+			werrstr("open ref: %r");
+			goto error;
+		}
+		if(fprint(fd, "%H", upd[i]) == -1){
+			werrstr("upate ref: %r");
+			close(fd);
+			goto error;
+		}
+		close(fd);
+	}
+	/*
+	 * Heuristic:
+	 * If there are no valid refs, and HEAD is invalid, then
+	 * pick the ref with the newest commits as the default
+	 * branch.
+	 *
+	 * Several people have been caught out by pushing to
+	 * a repo where HEAD named differently from what got
+	 * pushed, and this is going to be more of a footgun
+	 * when 'master', 'main', and 'front' are all in active
+	 * use. This should make us pick a useful default in
+	 * those cases, instead of silently failing.
+	 */
+	if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){
+		if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){
+			werrstr("open HEAD: %r");
+			goto error;
+		}
+		if(fprint(fd, "ref: %s", ref[0]) == -1){
+			werrstr("write HEAD ref: %r");
+			goto error;
+		}
+		close(fd);
+	}
+	ret = 0;
+error:
+	fmtpkt(c, "ERR %r");
+	close(lockfd);
+	return ret;
+}
+
+int
+recvpack(Conn *c)
+{
+	Hash *cur, *upd;
+	char **ref;
+	int nupd;
+
+	if(recvnegotiate(c, &cur, &upd, &ref, &nupd) == -1)
+		sysfatal("negotiate refs: %r");
+	if(nupd != 0 && updatepack(c) == -1)
+		sysfatal("update pack: %r");
+	if(nupd != 0 && updaterefs(c, cur, upd, ref, nupd) == -1)
+		sysfatal("update refs: %r");
+	return 0;
+}
+
+char*
+parsecmd(char *buf, char *cmd, int ncmd)
+{
+	int i;
+	char *p;
+
+	for(p = buf, i = 0; *p && i < ncmd - 1; i++, p++){
+		if(*p == ' ' || *p == '\t'){
+			cmd[i] = 0;
+			break;
+		}
+		cmd[i] = *p;
+	}
+	while(*p == ' ' || *p == '\t')
+		p++;
+	return p;
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-dw] [-r rel]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *repo, cmd[32], buf[512];
+	char *user;
+	Conn c;
+
+	ARGBEGIN{
+	case 'd':
+		chattygit++;
+		break;
+	case 'r':
+		pathpfx = EARGF(usage());
+		if(*pathpfx != '/')
+			sysfatal("path prefix must begin with '/'");
+		break;
+	case 'n':
+		namespace=EARGF(usage());
+		break;
+	case 'w':
+		allowwrite++;
+		break;
+	default:
+		usage();
+		break;
+	}ARGEND;
+
+	gitinit();
+	user = "none";
+	interactive = 0;
+	if(allowwrite)
+		user = getuser();
+	if(newns(user, namespace) == -1)
+		sysfatal("addns: %r");
+	if(bind(pathpfx, "/", MREPL) == -1)
+		sysfatal("bind: %r");
+	if(rfork(RFNOMNT) == -1)
+		sysfatal("rfork: %r");
+
+	initconn(&c, 0, 1);
+	if(readpkt(&c, buf, sizeof(buf)) == -1)
+		sysfatal("readpkt: %r");
+	repo = parsecmd(buf, cmd, sizeof(cmd));
+	cleanname(repo);
+	if(strncmp(repo, "../", 3) == 0)
+		sysfatal("invalid path %s\n", repo);
+	if(bind(repo, "/", MREPL) == -1){
+		fmtpkt(&c, "ERR no repo %r\n");
+		sysfatal("enter %s: %r", repo);
+	}
+	if(chdir("/") == -1)
+		sysfatal("chdir: %r");
+	if(access(".git", AREAD) == -1)
+		sysfatal("no git repository");
+	if(strcmp(cmd, "git-receive-pack") == 0 && allowwrite)
+		recvpack(&c);
+	else if(strcmp(cmd, "git-upload-pack") == 0)
+		servpack(&c);
+	else
+		sysfatal("unsupported command '%s'", cmd);
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/cmd/git/util.c
@@ -1,0 +1,321 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+Reprog *authorpat;
+Hash Zhash;
+
+int chattygit;
+int interactive = 1;
+
+Object*
+emptydir(void)
+{
+	static Object *e;
+
+	if(e != nil)
+		return ref(e);
+	e = emalloc(sizeof(Object));
+	e->hash = Zhash;
+	e->type = GTree;
+	e->tree = emalloc(sizeof(Tinfo));
+	e->tree->ent = nil;
+	e->tree->nent = 0;
+	e->flag |= Cloaded|Cparsed;
+	e->off = -1;
+	ref(e);
+	cache(e);
+	return e;
+}
+
+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 *
+eamalloc(ulong n, ulong sz)
+{
+	uvlong na;
+	void *v;
+
+	if((na = (uvlong)n*(uvlong)sz) >= (1ULL<<30))
+		sysfatal("alloc: overflow");
+	v = mallocz(na, 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(&p));
+	return v;
+}
+
+void *
+earealloc(void *p, ulong n, ulong sz)
+{
+	uvlong na;
+	void *v;
+
+	if((na = (uvlong)n*(uvlong)sz) >= (1ULL<<30))
+		sysfatal("alloc: overflow");
+	v = realloc(p, na);
+	if(v == nil)
+		sysfatal("realloc: %r");
+	setmalloctag(v, getcallerpc(&p));
+	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)
+{
+	int t;
+	int l;
+
+	t = va_arg(fmt->args, int);
+	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
+_dprint(char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	vfprint(2, fmt, ap);
+	va_end(ap);
+}
+
+/* Finds the directory containing the git repo. */
+int
+findrepo(char *buf, int nbuf)
+{
+	char *p, *suff;
+
+	suff = "/.git/HEAD";
+	if(getwd(buf, nbuf - strlen(suff) - 1) == nil)
+		return -1;
+
+	for(p = buf + strlen(buf); p != nil; p = strrchr(buf, '/')){
+		strcpy(p, suff);
+		if(access(buf, AEXIST) == 0){
+			p[p == buf] = '\0';
+			return 0;
+		}
+		*p = '\0';
+	}
+	werrstr("not a git repository");
+	return -1;
+}
+
+int
+showprogress(int x, int pct)
+{
+	if(!interactive)
+		return 0;
+	if(x > pct){
+		pct = x;
+		fprint(2, "\b\b\b\b%3d%%", pct);
+	}
+	return pct;
+}
--- /dev/null
+++ b/sys/src/cmd/git/walk.c
@@ -1,0 +1,333 @@
+#include <u.h>
+#include <libc.h>
+#include "git.h"
+
+#define NCACHE 4096
+#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;
+}
+
+void
+grow(Wres *r)
+{
+	if(r->npath == r->pathsz){
+		r->pathsz = 2*r->pathsz + 1;
+		r->path = erealloc(r->path, r->pathsz * sizeof(char*));
+	}
+}
+
+int
+readpaths(Wres *r, char *pfx, char *dir)
+{
+	char *f, *sub, *full, *sep;
+	Dir *d;
+	int fd, ret, i, n;
+
+	d = nil;
+	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{
+				grow(r);
+				if((f = smprint("%s%s%s", dir, sep, d[i].name)) == nil)
+					sysfatal("smprint: %r");
+				r->path[r->npath++] = f;
+			}
+		}
+		free(d);
+	}
+	ret = r->npath;
+error:
+	close(fd);
+	free(full);
+	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;
+}
+
+int
+sameqid(Dir *d, char *qf)
+{
+	char indexqid[64], fileqid[64], *p;
+	int fd, n;
+
+	if(!d)
+		return 0;
+	if((fd = open(qf, OREAD)) == -1)
+		return 0;
+	if((n = readn(fd, indexqid, sizeof(indexqid) - 1)) == -1)
+		return 0;
+	indexqid[n] = 0;
+	close(fd);
+	if((p = strpbrk(indexqid, "  \t\n\r")) != nil)
+		*p = 0;
+
+	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;
+}
+
+void
+writeqid(Dir *d, char *qf)
+{
+	int fd;
+
+	if((fd = create(qf, OWRITE, 0666)) == -1)
+		return;
+	fprint(fd, "%ullx.%uld.%.2uhhx\n",
+		d->qid.path, d->qid.vers, d->qid.type);
+	close(fd);
+}
+
+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] [paths...]\n", argv0);
+	exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+	char *rpath, *tpath, *bpath, buf[8], repo[512];
+	char *p, *e;
+	int i, dirty;
+	Wres r;
+	Dir *d;
+
+	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
+
+	if(access("/mnt/git/ctl", AEXIST) != 0)
+		sysfatal("no running git/fs");
+	if(findrepo(repo, sizeof(repo)) == -1)
+		sysfatal("find root: %r");
+	if(chdir(repo) == -1)
+		sysfatal("chdir: %r");
+	dirty = 0;
+	memset(&r, 0, sizeof(r));
+	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(argc == 0){
+		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");
+	}else{
+		for(i = 0; i < argc; i++){
+			tpath = smprint(TDIR"/%s", argv[i]);
+			rpath = smprint(RDIR"/%s", argv[i]);
+			if((d = dirstat(tpath)) == nil && (d = dirstat(rpath)) == nil)
+				goto nextarg;
+			if(d->mode & DMDIR){
+				readpaths(&r, TDIR, argv[i]);
+				readpaths(&r, RDIR, argv[i]);
+			}else{
+				grow(&r);
+				r.path[r.npath++] = estrdup(argv[i]);
+			}
+nextarg:
+			free(tpath);
+			free(rpath);
+			free(d);
+		}
+	}
+	dedup(&r);
+
+	for(i = 0; i < r.npath; i++){
+		p = r.path[i];
+		d = dirstat(p);
+		if(d && d->mode & DMDIR)
+			goto next;
+		rpath = smprint(RDIR"/%s", p);
+		tpath = smprint(TDIR"/%s", p);
+		bpath = smprint(HDIR"/%s", p);
+		/* Fast path: we don't want to force access to the rpath. */
+		if(d && sameqid(d, tpath)) {
+			if(!quiet && (printflg & Tflg))
+				print("%s%s\n", tstr, p);
+		}else{
+			if(d == nil || access(rpath, AEXIST) == 0){
+				dirty |= Rflg;
+				if(!quiet && (printflg & Rflg))
+					print("%s%s\n", rstr, p);
+			}else if(access(bpath, AEXIST) == -1) {
+				dirty |= Aflg;
+				if(!quiet && (printflg & Aflg))
+					print("%s%s\n", astr, p);
+			}else if(samedata(p, bpath)){
+				if(!quiet && (printflg & Tflg))
+					print("%s%s\n", tstr, p);
+				writeqid(d, tpath);
+			}else{
+				dirty |= Mflg;
+				if(!quiet && (printflg & Mflg))
+					print("%s%s\n", mstr, p);
+			}
+		}
+		free(rpath);
+		free(tpath);
+		free(bpath);
+next:
+		free(d);
+	}
+	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);
+}