~mcf/plan9front

5d2969bc92a81e6ba0d8a0a4873490145de96845 — Ori Bernstein 4 months ago 2c776d9
git: got git?

Add a snapshot of git9 to 9front.
A sys/man/1/git => sys/man/1/git +643 -0
@@ 0,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.

A sys/man/4/gitfs => sys/man/4/gitfs +112 -0
@@ 0,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.



A sys/src/cmd/git/add => sys/src/cmd/git/add +39 -0
@@ 0,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 ''

A sys/src/cmd/git/branch => sys/src/cmd/git/branch +109 -0
@@ 0,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 ''

A sys/src/cmd/git/clone => sys/src/cmd/git/clone +115 -0
@@ 0,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 ''

A sys/src/cmd/git/commit => sys/src/cmd/git/commit +150 -0
@@ 0,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 ''

A sys/src/cmd/git/compat => sys/src/cmd/git/compat +158 -0
@@ 0,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 ''

A sys/src/cmd/git/conf.c => sys/src/cmd/git/conf.c +97 -0
@@ 0,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);
}

A sys/src/cmd/git/delta.c => sys/src/cmd/git/delta.c +219 -0
@@ 0,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;
}

A sys/src/cmd/git/diff => sys/src/cmd/git/diff +37 -0
@@ 0,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 ''

A sys/src/cmd/git/export => sys/src/cmd/git/export +89 -0
@@ 0,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 ''

A sys/src/cmd/git/fetch.c => sys/src/cmd/git/fetch.c +316 -0
@@ 0,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);
}

A sys/src/cmd/git/fs.c => sys/src/cmd/git/fs.c +853 -0
@@ 0,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);
}

A sys/src/cmd/git/git.h => sys/src/cmd/git/git.h +303 -0
@@ 0,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 *);

A sys/src/cmd/git/import => sys/src/cmd/git/import +99 -0
@@ 0,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 ''

A sys/src/cmd/git/init => sys/src/cmd/git/init +38 -0
@@ 0,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 ''

A sys/src/cmd/git/log.c => sys/src/cmd/git/log.c +329 -0
@@ 0,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);
}

A sys/src/cmd/git/merge => sys/src/cmd/git/merge +47 -0
@@ 0,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 ''

A sys/src/cmd/git/mkfile => sys/src/cmd/git/mkfile +57 -0
@@ 0,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

A sys/src/cmd/git/objset.c => sys/src/cmd/git/objset.c +67 -0
@@ 0,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;
}

A sys/src/cmd/git/ols.c => sys/src/cmd/git/ols.c +170 -0
@@ 0,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;
}

A sys/src/cmd/git/pack.c => sys/src/cmd/git/pack.c +1712 -0
@@ 0,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;