]> git.lizzy.rs Git - plan9front.git/commitdiff
git: got git?
authorOri Bernstein <ori@eigenstate.org>
Mon, 17 May 2021 01:49:45 +0000 (18:49 -0700)
committerOri Bernstein <ori@eigenstate.org>
Mon, 17 May 2021 01:49:45 +0000 (18:49 -0700)
Add a snapshot of git9 to 9front.

36 files changed:
sys/man/1/git [new file with mode: 0644]
sys/man/4/gitfs [new file with mode: 0644]
sys/src/cmd/git/add [new file with mode: 0755]
sys/src/cmd/git/branch [new file with mode: 0755]
sys/src/cmd/git/clone [new file with mode: 0755]
sys/src/cmd/git/commit [new file with mode: 0755]
sys/src/cmd/git/compat [new file with mode: 0755]
sys/src/cmd/git/conf.c [new file with mode: 0644]
sys/src/cmd/git/delta.c [new file with mode: 0644]
sys/src/cmd/git/diff [new file with mode: 0755]
sys/src/cmd/git/export [new file with mode: 0755]
sys/src/cmd/git/fetch.c [new file with mode: 0644]
sys/src/cmd/git/fs.c [new file with mode: 0644]
sys/src/cmd/git/git.h [new file with mode: 0644]
sys/src/cmd/git/import [new file with mode: 0755]
sys/src/cmd/git/init [new file with mode: 0755]
sys/src/cmd/git/log.c [new file with mode: 0644]
sys/src/cmd/git/merge [new file with mode: 0755]
sys/src/cmd/git/mkfile [new file with mode: 0644]
sys/src/cmd/git/objset.c [new file with mode: 0644]
sys/src/cmd/git/ols.c [new file with mode: 0644]
sys/src/cmd/git/pack.c [new file with mode: 0644]
sys/src/cmd/git/proto.c [new file with mode: 0644]
sys/src/cmd/git/pull [new file with mode: 0755]
sys/src/cmd/git/push [new file with mode: 0755]
sys/src/cmd/git/query.c [new file with mode: 0644]
sys/src/cmd/git/rebase [new file with mode: 0755]
sys/src/cmd/git/ref.c [new file with mode: 0644]
sys/src/cmd/git/repack.c [new file with mode: 0644]
sys/src/cmd/git/revert [new file with mode: 0644]
sys/src/cmd/git/rm [new file with mode: 0755]
sys/src/cmd/git/save.c [new file with mode: 0644]
sys/src/cmd/git/send.c [new file with mode: 0644]
sys/src/cmd/git/serve.c [new file with mode: 0644]
sys/src/cmd/git/util.c [new file with mode: 0644]
sys/src/cmd/git/walk.c [new file with mode: 0644]

diff --git a/sys/man/1/git b/sys/man/1/git
new file mode 100644 (file)
index 0000000..7b04c03
--- /dev/null
@@ -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.
diff --git a/sys/man/4/gitfs b/sys/man/4/gitfs
new file mode 100644 (file)
index 0000000..00c68ed
--- /dev/null
@@ -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.
+
+
diff --git a/sys/src/cmd/git/add b/sys/src/cmd/git/add
new file mode 100755 (executable)
index 0000000..cfe08dd
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/branch b/sys/src/cmd/git/branch
new file mode 100755 (executable)
index 0000000..b4dac71
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/clone b/sys/src/cmd/git/clone
new file mode 100755 (executable)
index 0000000..52346e3
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/commit b/sys/src/cmd/git/commit
new file mode 100755 (executable)
index 0000000..7b754bb
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/compat b/sys/src/cmd/git/compat
new file mode 100755 (executable)
index 0000000..15eb261
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/conf.c b/sys/src/cmd/git/conf.c
new file mode 100644 (file)
index 0000000..cecc1b6
--- /dev/null
@@ -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);
+}
diff --git a/sys/src/cmd/git/delta.c b/sys/src/cmd/git/delta.c
new file mode 100644 (file)
index 0000000..32af58b
--- /dev/null
@@ -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;
+}
diff --git a/sys/src/cmd/git/diff b/sys/src/cmd/git/diff
new file mode 100755 (executable)
index 0000000..be74676
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/export b/sys/src/cmd/git/export
new file mode 100755 (executable)
index 0000000..145e625
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/fetch.c b/sys/src/cmd/git/fetch.c
new file mode 100644 (file)
index 0000000..727cc3c
--- /dev/null
@@ -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);
+}
diff --git a/sys/src/cmd/git/fs.c b/sys/src/cmd/git/fs.c
new file mode 100644 (file)
index 0000000..1f4d5c9
--- /dev/null
@@ -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);
+}
diff --git a/sys/src/cmd/git/git.h b/sys/src/cmd/git/git.h
new file mode 100644 (file)
index 0000000..76c215b
--- /dev/null
@@ -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 *);
diff --git a/sys/src/cmd/git/import b/sys/src/cmd/git/import
new file mode 100755 (executable)
index 0000000..31103cd
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/init b/sys/src/cmd/git/init
new file mode 100755 (executable)
index 0000000..b8e4813
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/log.c b/sys/src/cmd/git/log.c
new file mode 100644 (file)
index 0000000..faace2a
--- /dev/null
@@ -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);
+}
diff --git a/sys/src/cmd/git/merge b/sys/src/cmd/git/merge
new file mode 100755 (executable)
index 0000000..5517a39
--- /dev/null
@@ -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 ''
diff --git a/sys/src/cmd/git/mkfile b/sys/src/cmd/git/mkfile
new file mode 100644 (file)
index 0000000..610d3af
--- /dev/null
@@ -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
diff --git a/sys/src/cmd/git/objset.c b/sys/src/cmd/git/objset.c
new file mode 100644 (file)
index 0000000..d0c656c
--- /dev/null
@@ -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;
+}
diff --git a/sys/src/cmd/git/ols.c b/sys/src/cmd/git/ols.c
new file mode 100644 (file)
index 0000000..3b77d37
--- /dev/null
@@ -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;
+}
diff --git a/sys/src/cmd/git/pack.c b/sys/src/cmd/git/pack.c
new file mode 100644 (file)
index 0000000..847d931
--- /dev/null
@@ -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;
+
+       pct = 0;
+       dprint(1, "picking deltas\n");
+       fprint(2, "deltifying %d objects:   0%%", nmeta);
+       qsort(meta, nmeta, sizeof(Meta*), deltaordercmp);
+       for(i = 0; i < nmeta; i++){
+               m = meta[i];
+               pct = showprogress((i*100) / nmeta, pct);
+               m->delta = nil;
+               m->ndelta = 0;
+               if(m->obj->type == GCommit || m->obj->type == GTag)
+                       continue;
+               if((o = readobject(m->obj->hash)) == nil)
+                       sysfatal("readobject %H: %r", m->obj->hash);
+               dtinit(&m->dtab, o);
+               if(i >= 11)
+                       dtclear(&meta[i-11]->dtab);
+               best = o->size;
+               for(j = max(0, i - 10); j < i; j++){
+                       p = meta[j];
+                       /* long chains make unpacking slow */
+                       if(p->nchain >= 128 || p->obj->type != o->type)
+                               continue;
+                       d = deltify(o, &p->dtab, &nd);
+                       sz = deltasz(d, nd);
+                       if(sz + 32 < best){
+                               /*
+                                * if we already picked a best delta,
+                                * replace it.
+                                */
+                               free(m->delta);
+                               best = sz;
+                               m->delta = d;
+                               m->ndelta = nd;
+                               m->nchain = p->nchain + 1;
+                               m->prev = p;
+                               m->head = p->head;
+                               if(m->head == nil)
+                                       m->head = p;
+                       }else
+                               free(d);
+               }
+               unref(o);
+       }
+       for(i = max(0, nmeta - 10); i < nmeta; i++)
+               dtclear(&meta[i]->dtab);
+       fprint(2, "\b\b\b\b100%%\n");
+}
+
+static int
+compread(void *p, void *dst, int n)
+{
+       Buf *b;
+
+       b = p;
+       if(n > b->sz - b->off)
+               n = b->sz - b->off;
+       memcpy(dst, b->data + b->off, n);
+       b->off += n;
+       return n;
+}
+
+static int
+compwrite(void *p, void *buf, int n)
+{
+       return hwrite(((Compout *)p)->bfd, buf, n, &((Compout*)p)->st);
+}
+
+static int
+hcompress(Biobuf *bfd, void *buf, int sz, DigestState **st)
+{
+       int r;
+       Buf b ={
+               .off=0,
+               .data=buf,
+               .sz=sz,
+       };
+       Compout o = {
+               .bfd = bfd,
+               .st = *st,
+       };
+
+       r = deflatezlib(&o, compwrite, &b, compread, 6, 0);
+       *st = o.st;
+       return r;
+}
+
+static void
+append(char **p, int *len, int *sz, void *seg, int nseg)
+{
+       if(*len + nseg >= *sz){
+               while(*len + nseg >= *sz)
+                       *sz += *sz/2;
+               *p = erealloc(*p, *sz);
+       }
+       memcpy(*p + *len, seg, nseg);
+       *len += nseg;
+}
+
+static int
+encodedelta(Meta *m, Object *o, Object *b, void **pp)
+{
+       char *p, *bp, buf[16];
+       int len, sz, n, i, j;
+       Delta *d;
+
+       sz = 128;
+       len = 0;
+       p = emalloc(sz);
+
+       /* base object size */
+       buf[0] = b->size & 0x7f;
+       n = b->size >> 7;
+       for(i = 1; n > 0; i++){
+               buf[i - 1] |= 0x80;
+               buf[i] = n & 0x7f;
+               n >>= 7;
+       }
+       append(&p, &len, &sz, buf, i);
+
+       /* target object size */
+       buf[0] = o->size & 0x7f;
+       n = o->size >> 7;
+       for(i = 1; n > 0; i++){
+               buf[i - 1] |= 0x80;
+               buf[i] = n & 0x7f;
+               n >>= 7;
+       }
+       append(&p, &len, &sz, buf, i);
+       for(j = 0; j < m->ndelta; j++){
+               d = &m->delta[j];
+               if(d->cpy){
+                       n = d->off;
+                       bp = buf + 1;
+                       buf[0] = 0x81;
+                       buf[1] = 0x00;
+                       for(i = 0; i < sizeof(buf); i++) {
+                               buf[0] |= 1<<i;
+                               *bp++ = n & 0xff;
+                               n >>= 8;
+                               if(n == 0)
+                                       break;
+                       }
+
+                       n = d->len;
+                       if(n != 0x10000) {
+                               buf[0] |= 0x1<<4;
+                               for(i = 0; i < sizeof(buf)-4 && n > 0; i++){
+                                       buf[0] |= 1<<(i + 4);
+                                       *bp++ = n & 0xff;
+                                       n >>= 8;
+                               }
+                       }
+                       append(&p, &len, &sz, buf, bp - buf);
+               }else{
+                       n = 0;
+                       while(n != d->len){
+                               buf[0] = (d->len - n < 127) ? d->len - n : 127;
+                               append(&p, &len, &sz, buf, 1);
+                               append(&p, &len, &sz, o->data + d->off + n, buf[0]);
+                               n += buf[0];
+                       }
+               }
+       }
+       *pp = p;
+       return len;
+}
+
+static int
+packhdr(char *hdr, int ty, int len)
+{
+       int i;
+
+       hdr[0] = ty << 4;
+       hdr[0] |= len & 0xf;
+       len >>= 4;
+       for(i = 1; len != 0; i++){
+               hdr[i-1] |= 0x80;
+               hdr[i] = len & 0x7f;
+               len >>= 7;
+       }
+       return i;
+}
+
+static int
+packoff(char *hdr, vlong off)
+{
+       int i, j;
+       char rbuf[8];
+
+       rbuf[0] = off & 0x7f;
+       for(i = 1; (off >>= 7) != 0; i++)
+               rbuf[i] = (--off & 0x7f)|0x80;
+
+       j = 0;
+       while(i > 0)
+               hdr[j++] = rbuf[--i];
+       return j;
+}
+
+static int
+genpack(int fd, Meta **meta, int nmeta, Hash *h, int odelta)
+{
+       int i, nh, nd, res, pct, ret;
+       DigestState *st;
+       Biobuf *bfd;
+       Meta *m;
+       Object *o, *b;
+       char *p, buf[32];
+
+       st = nil;
+       ret = -1;
+       pct = 0;
+       dprint(1, "generating pack\n");
+       if((fd = dup(fd, -1)) == -1)
+               return -1;
+       if((bfd = Bfdopen(fd, OWRITE)) == nil)
+               return -1;
+       if(hwrite(bfd, "PACK", 4, &st) == -1)
+               return -1;
+       PUTBE32(buf, 2);
+       if(hwrite(bfd, buf, 4, &st) == -1)
+               return -1;
+       PUTBE32(buf, nmeta);
+       if(hwrite(bfd, buf, 4, &st) == -1)
+               return -1;
+       qsort(meta, nmeta, sizeof(Meta*), writeordercmp);
+       if(interactive)
+               fprint(2, "writing %d objects:   0%%", nmeta);
+       for(i = 0; i < nmeta; i++){
+               pct = showprogress((i*100)/nmeta, pct);
+               m = meta[i];
+               m->off = Boffset(bfd);
+               if((o = readobject(m->obj->hash)) == nil)
+                       return -1;
+               if(m->delta == nil){
+                       nh = packhdr(buf, o->type, o->size);
+                       hwrite(bfd, buf, nh, &st);
+                       if(hcompress(bfd, o->data, o->size, &st) == -1)
+                               goto error;
+               }else{
+                       b = readobject(m->prev->obj->hash);
+                       nd = encodedelta(m, o, b, &p);
+                       unref(b);
+                       if(odelta && m->prev->off != 0){
+                               nh = 0;
+                               nh += packhdr(buf, GOdelta, nd);
+                               nh += packoff(buf+nh, m->off - m->prev->off);
+                               hwrite(bfd, buf, nh, &st);
+                       }else{
+                               nh = packhdr(buf, GRdelta, nd);
+                               hwrite(bfd, buf, nh, &st);
+                               hwrite(bfd, m->prev->obj->hash.h, sizeof(m->prev->obj->hash.h), &st);
+                       }
+                       res = hcompress(bfd, p, nd, &st);
+                       free(p);
+                       if(res == -1)
+                               goto error;
+               }
+               unref(o);
+       }
+       if(interactive)
+               fprint(2, "\b\b\b\b100%%\n");
+       sha1(nil, 0, h->h, st);
+       if(Bwrite(bfd, h->h, sizeof(h->h)) == -1)
+               goto error;
+       ret = 0;
+error:
+       if(Bterm(bfd) == -1)
+               return -1;
+       return ret;
+}
+
+int
+writepack(int fd, Hash *theirs, int ntheirs, Hash *ours, int nours, Hash *h)
+{
+       Meta **meta;
+       int i, r, nmeta;
+
+       if((nmeta = readmeta(theirs, ntheirs, ours, nours, &meta)) == -1)
+               return -1;
+       pickdeltas(meta, nmeta);
+       r = genpack(fd, meta, nmeta, h, 0);
+       for(i = 0; i < nmeta; i++)
+               freemeta(meta[i]);
+       free(meta);
+       return r;
+}
diff --git a/sys/src/cmd/git/proto.c b/sys/src/cmd/git/proto.c
new file mode 100644 (file)
index 0000000..19c0889
--- /dev/null
@@ -0,0 +1,459 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+#define Useragent      "useragent git/2.24.1"
+#define Contenthdr     "headers Content-Type: application/x-git-%s-pack-request"
+#define Accepthdr      "headers Accept: application/x-git-%s-pack-result"
+
+enum {
+       Nproto  = 16,
+       Nport   = 16,
+       Nhost   = 256,
+       Npath   = 128,
+       Nrepo   = 64,
+       Nbranch = 32,
+};
+
+void
+tracepkt(int v, char *pfx, char *b, int n)
+{
+       char *f;
+       int o, i;
+
+       if(chattygit < v)
+               return;
+       o = 0;
+       f = emalloc(n*4 + 1);
+       for(i = 0; i < n; i++){
+               if(isprint(b[i])){
+                       f[o++] = b[i];
+                       continue;
+               }
+               f[o++] = '\\';
+               switch(b[i]){
+               case '\\':      f[o++] = '\\';  break;
+               case '\n':      f[o++] = 'n';   break;
+               case '\r':      f[o++] = 'r';   break;
+               case '\v':      f[o++] = 'v';   break;
+               case '\0':      f[o++] = '0';   break;
+               default:
+                       f[o++] = 'x';
+                       f[o++] = "0123456789abcdef"[(b[i]>>4)&0xf];
+                       f[o++] = "0123456789abcdef"[(b[i]>>0)&0xf];
+                       break;
+               }
+       }
+       f[o] = '\0';
+       fprint(2, "%s %04x:\t%s\n", pfx, n, f);
+       free(f);
+}
+
+int
+readpkt(Conn *c, char *buf, int nbuf)
+{
+       char len[5];
+       char *e;
+       int n;
+
+       if(readn(c->rfd, len, 4) == -1)
+               return -1;
+       len[4] = 0;
+       n = strtol(len, &e, 16);
+       if(n == 0){
+               dprint(1, "=r=> 0000\n");
+               return 0;
+       }
+       if(e != len + 4 || n <= 4)
+               sysfatal("pktline: bad length '%s'", len);
+       n  -= 4;
+       if(n >= nbuf)
+               sysfatal("pktline: undersize buffer");
+       if(readn(c->rfd, buf, n) != n)
+               return -1;
+       buf[n] = 0;
+       tracepkt(1, "=r=>", buf, n);
+       return n;
+}
+
+int
+writepkt(Conn *c, char *buf, int nbuf)
+{
+       char len[5];
+
+
+       snprint(len, sizeof(len), "%04x", nbuf + 4);
+       if(write(c->wfd, len, 4) != 4)
+               return -1;
+       if(write(c->wfd, buf, nbuf) != nbuf)
+               return -1;
+       tracepkt(1, "<=w=", buf, nbuf);
+       return 0;
+}
+
+int
+flushpkt(Conn *c)
+{
+       dprint(1, "<=w= 0000\n");
+       return write(c->wfd, "0000", 4);
+}
+
+static void
+grab(char *dst, int n, char *p, char *e)
+{
+       int l;
+
+       l = e - p;
+       if(l >= n)
+               sysfatal("overlong component");
+       memcpy(dst, p, l);
+       dst[l] = 0;
+}
+
+static int
+parseuri(char *uri, char *proto, char *host, char *port, char *path, char *repo)
+{
+       char *s, *p, *q;
+       int n, hasport;
+       print("uri: \"%s\"\n", uri);
+
+       p = strstr(uri, "://");
+       if(p == nil)
+               snprint(proto, Nproto, "ssh");
+       else if(strncmp(uri, "git+", 4) == 0)
+               grab(proto, Nproto, uri + 4, p);
+       else
+               grab(proto, Nproto, uri, p);
+       *port = 0;
+       hasport = 1;
+       if(strcmp(proto, "git") == 0)
+               snprint(port, Nport, "9418");
+       else if(strncmp(proto, "https", 5) == 0)
+               snprint(port, Nport, "443");
+       else if(strncmp(proto, "http", 4) == 0)
+               snprint(port, Nport, "80");
+       else if(strncmp(proto, "hjgit", 5) == 0)
+               snprint(port, Nport, "17021");
+       else if(strncmp(proto, "gits", 5) == 0)
+               snprint(port, Nport, "9419");
+       else
+               hasport = 0;
+       s = (p != nil) ? p + 3 : uri;
+       p = nil;
+       if(!hasport){
+               p = strstr(s, ":");
+               if(p != nil)
+                       p++;
+       }
+       if(p == nil)
+               p = strstr(s, "/");
+       if(p == nil || strlen(p) == 1){
+               werrstr("missing path");
+               return -1;
+       }
+
+       q = memchr(s, ':', p - s);
+       if(q){
+               grab(host, Nhost, s, q);
+               grab(port, Nport, q + 1, p);
+       }else{
+               grab(host, Nhost, s, p);
+       }
+       
+       snprint(path, Npath, "%s", p);
+       if((q = strrchr(p, '/')) != nil)
+               p = q + 1;
+       if(strlen(p) == 0){
+               werrstr("missing repository in uri");
+               return -1;
+       }
+       n = strlen(p);
+       if(hassuffix(p, ".git"))
+               n -= 4;
+       grab(repo, Nrepo, p, p + n);
+       return 0;
+}
+
+static int
+webclone(Conn *c, char *url)
+{
+       char buf[16];
+       int n, conn;
+
+       if((c->cfd = open("/mnt/web/clone", ORDWR)) < 0)
+               goto err;
+       if((n = read(c->cfd, buf, sizeof(buf)-1)) == -1)
+               goto err;
+       buf[n] = 0;
+       conn = atoi(buf);
+
+       /* github will behave differently based on useragent */
+       if(write(c->cfd, Useragent, sizeof(Useragent)) == -1)
+               return -1;
+       dprint(1, "open url %s\n", url);
+       if(fprint(c->cfd, "url %s", url) == -1)
+               goto err;
+       free(c->dir);
+       c->dir = smprint("/mnt/web/%d", conn);
+       return 0;
+err:
+       if(c->cfd != -1)
+               close(c->cfd);
+       return -1;
+}
+
+static int
+webopen(Conn *c, char *file, int mode)
+{
+       char path[128];
+       int fd;
+
+       snprint(path, sizeof(path), "%s/%s", c->dir, file);
+       if((fd = open(path, mode)) == -1)
+               return -1;
+       return fd;
+}
+
+static int
+issmarthttp(Conn *c, char *direction)
+{
+       char buf[Pktmax+1], svc[128];
+       int n;
+
+       if((n = readpkt(c, buf, sizeof(buf))) == -1)
+               sysfatal("http read: %r");
+       buf[n] = 0;
+       snprint(svc, sizeof(svc), "# service=git-%s-pack\n", direction);
+       if(strncmp(svc, buf, n) != 0){
+               werrstr("dumb http protocol not supported");
+               return -1;
+       }
+       if(readpkt(c, buf, sizeof(buf)) != 0){
+               werrstr("protocol garble: expected flushpkt");
+               return -1;
+       }
+       return 0;
+}
+
+static int
+dialhttp(Conn *c, char *host, char *port, char *path, char *direction)
+{
+       char *geturl, *suff, *hsep, *psep;
+
+       suff = "";
+       hsep = "";
+       psep = "";
+       if(port && strlen(port) != 0)
+               hsep = ":";
+       if(path && path[0] != '/')
+               psep = "/";
+       memset(c, 0, sizeof(*c));
+       geturl = smprint("https://%s%s%s%s%s%s/info/refs?service=git-%s-pack", host, hsep, port, psep, path, suff, direction);
+       c->type = ConnHttp;
+       c->url = smprint("https://%s%s%s%s%s%s/git-%s-pack", host, hsep, port, psep, path, suff, direction);
+       c->cfd = webclone(c, geturl);
+       free(geturl);
+       if(c->cfd == -1)
+               return -1;
+       c->rfd = webopen(c, "body", OREAD);
+       c->wfd = -1;
+       if(c->rfd == -1)
+               return -1;
+       if(issmarthttp(c, direction) == -1)
+               return -1;
+       c->direction = estrdup(direction);
+       return 0;
+}
+
+static int
+dialssh(Conn *c, char *host, char *, char *path, char *direction)
+{
+       int pid, pfd[2];
+       char cmd[64];
+
+       if(pipe(pfd) == -1)
+               sysfatal("unable to open pipe: %r");
+       pid = fork();
+       if(pid == -1)
+               sysfatal("unable to fork");
+       if(pid == 0){
+               close(pfd[1]);
+               dup(pfd[0], 0);
+               dup(pfd[0], 1);
+               snprint(cmd, sizeof(cmd), "git-%s-pack", direction);
+               dprint(1, "exec ssh '%s' '%s' %s\n", host, cmd, path);
+               execl("/bin/ssh", "ssh", host, cmd, path, nil);
+       }else{
+               close(pfd[0]);
+               c->type = ConnSsh;
+               c->rfd = pfd[1];
+               c->wfd = dup(pfd[1], -1);
+       }
+       return 0;
+}
+
+static int
+dialhjgit(Conn *c, char *host, char *port, char *path, char *direction, int auth)
+{
+       char *ds, *p, *e, cmd[512];
+       int pid, pfd[2];
+
+       if((ds = netmkaddr(host, "tcp", port)) == nil)
+               return -1;
+       if(pipe(pfd) == -1)
+               sysfatal("unable to open pipe: %r");
+       pid = fork();
+       if(pid == -1)
+               sysfatal("unable to fork");
+       if(pid == 0){
+               close(pfd[1]);
+               dup(pfd[0], 0);
+               dup(pfd[0], 1);
+               dprint(1, "exec tlsclient -a %s\n", ds);
+               if(auth)
+                       execl("/bin/tlsclient", "tlsclient", "-a", ds, nil);
+               else
+                       execl("/bin/tlsclient", "tlsclient", ds, nil);
+               sysfatal("exec: %r");
+       }else{
+               close(pfd[0]);
+               p = cmd;
+               e = cmd + sizeof(cmd);
+               p = seprint(p, e - 1, "git-%s-pack %s", direction, path);
+               p = seprint(p + 1, e, "host=%s", host);
+               c->type = ConnGit9;
+               c->rfd = pfd[1];
+               c->wfd = dup(pfd[1], -1);
+               if(writepkt(c, cmd, p - cmd + 1) == -1){
+                       fprint(2, "failed to write message\n");
+                       close(c->rfd);
+                       close(c->wfd);
+                       return -1;
+               }
+       }
+       return 0;
+}
+
+
+static int
+dialgit(Conn *c, char *host, char *port, char *path, char *direction)
+{
+       char *ds, *p, *e, cmd[512];
+       int fd;
+
+       if((ds = netmkaddr(host, "tcp", port)) == nil)
+               return -1;
+       dprint(1, "dial %s git-%s-pack %s\n", ds, direction, path);
+       fd = dial(ds, nil, nil, nil);
+       if(fd == -1)
+               return -1;
+       p = cmd;
+       e = cmd + sizeof(cmd);
+       p = seprint(p, e - 1, "git-%s-pack %s", direction, path);
+       p = seprint(p + 1, e, "host=%s", host);
+       c->type = ConnGit;
+       c->rfd = fd;
+       c->wfd = dup(fd, -1);
+       if(writepkt(c, cmd, p - cmd + 1) == -1){
+               fprint(2, "failed to write message\n");
+               close(fd);
+               return -1;
+       }
+       return 0;
+}
+
+void
+initconn(Conn *c, int rd, int wr)
+{
+       c->type = ConnGit;
+       c->rfd = rd;
+       c->wfd = wr;
+}
+
+int
+gitconnect(Conn *c, char *uri, char *direction)
+{
+       char proto[Nproto], host[Nhost], port[Nport];
+       char repo[Nrepo], path[Npath];
+
+       if(parseuri(uri, proto, host, port, path, repo) == -1){
+               werrstr("bad uri %s", uri);
+               return -1;
+       }
+
+       memset(c, 0, sizeof(Conn));
+       if(strcmp(proto, "ssh") == 0)
+               return dialssh(c, host, port, path, direction);
+       else if(strcmp(proto, "git") == 0)
+               return dialgit(c, host, port, path, direction);
+       else if(strcmp(proto, "hjgit") == 0)
+               return dialhjgit(c, host, port, path, direction, 1);
+       else if(strcmp(proto, "gits") == 0)
+               return dialhjgit(c, host, port, path, direction, 0);
+       else if(strcmp(proto, "http") == 0 || strcmp(proto, "https") == 0)
+               return dialhttp(c, host, port, path, direction);
+       werrstr("unknown protocol %s", proto);
+       return -1;
+}
+
+int
+writephase(Conn *c)
+{
+       char hdr[128];
+       int n;
+
+       dprint(1, "start write phase\n");
+       if(c->type != ConnHttp)
+               return 0;
+
+       if(c->wfd != -1)
+               close(c->wfd);
+       if(c->cfd != -1)
+               close(c->cfd);
+       if((c->cfd = webclone(c, c->url)) == -1)
+               return -1;
+       n = snprint(hdr, sizeof(hdr), Contenthdr, c->direction);
+       if(write(c->cfd, hdr, n) == -1)
+               return -1;
+       n = snprint(hdr, sizeof(hdr), Accepthdr, c->direction);
+       if(write(c->cfd, hdr, n) == -1)
+               return -1;
+       if((c->wfd = webopen(c, "postbody", OWRITE)) == -1)
+               return -1;
+       c->rfd = -1;
+       return 0;
+}
+
+int
+readphase(Conn *c)
+{
+       dprint(1, "start read phase\n");
+       if(c->type != ConnHttp)
+               return 0;
+       if(close(c->wfd) == -1)
+               return -1;
+       if((c->rfd = webopen(c, "body", OREAD)) == -1)
+               return -1;
+       c->wfd = -1;
+       return 0;
+}
+
+void
+closeconn(Conn *c)
+{
+       close(c->rfd);
+       close(c->wfd);
+       switch(c->type){
+       case ConnGit:
+               break;
+       case ConnGit9:
+       case ConnSsh:
+               free(wait());
+               break;
+       case ConnHttp:
+               close(c->cfd);
+               break;
+       }
+}
diff --git a/sys/src/cmd/git/pull b/sys/src/cmd/git/pull
new file mode 100755 (executable)
index 0000000..4dd7fe9
--- /dev/null
@@ -0,0 +1,82 @@
+#!/bin/rc -e
+rfork en
+. /sys/lib/git/common.rc
+
+fn update{
+       branch=$1
+       upstream=$2
+       url=$3
+       dir=$4
+       bflag=()
+       dflag=()
+       if(! ~ $#branch 0)
+               bflag=(-b $branch)
+       if(! ~ $#debug 0)
+               dflag='-d'
+       {git/fetch $dflag $bflag -u $upstream $url >[2=3] || die $status} | awk '
+       /^remote/{
+               if($2=="HEAD")
+                       next
+               ref=$2
+               hash=$3
+               gsub("^refs/heads", "refs/remotes/'$upstream'", ref)
+               outfile = ".git/"ref
+               system("mkdir -p `{basename -d "outfile"}");
+               print hash > outfile;
+               close(outfile);
+       }
+       ' |[3] tr '\x0d' '\x0a'
+}
+
+gitup
+
+flagfmt='a:allbranch, b:branch branch, d:debug,
+       f:fetchonly, u:upstream upstream, q:quiet'
+args=''
+eval `''{aux/getflags $*} || exec aux/usage
+
+if(~ $#branch 0)
+       branch=refs/`{git/branch}
+if(~ $allbranch 1)
+       branch=''
+
+if(~ $#upstream 0)
+       upstream=origin
+remote=`$nl{git/conf 'remote "'$upstream'".url'}
+if(~ $#remote 0){
+       remote=$upstream
+       upstream=THEM
+}
+
+update $branch $upstream $remote
+if (~ $fetchonly 1)
+       exit
+
+local=`{git/branch}
+remote=`{git/branch | subst '^(refs/)?heads' 'remotes/'$upstream}
+
+# we have local commits, but the remote hasn't changed.
+# in this case, we want to keep the local commits untouched.
+if(~ `{git/query HEAD $remote @} `{git/query $remote}){
+       echo 'up to date' >[1=2]
+       exit
+}
+# The remote repository and our HEAD have diverged: we
+# need to merge.
+if(! ~ `{git/query HEAD $remote @} `{git/query HEAD}){
+       >[1=2]{
+               echo ours:      `{git/query HEAD}
+               echo theirs:    `{git/query $remote}
+               echo common:    `{git/query HEAD $remote @}
+               echo git/merge $remote
+       }
+       exit diverged
+}
+# The remote is directly ahead of the local, and we have
+# no local commits that need merging.
+if(~ $#quiet 0)
+       git/log -s -e $local'..'$remote >[1=2]
+echo
+echo $remote':' `{git/query $local} '=>' `{git/query $remote}  >[1=2]
+git/branch -mnb $remote $local
+exit ''
diff --git a/sys/src/cmd/git/push b/sys/src/cmd/git/push
new file mode 100755 (executable)
index 0000000..b7a9564
--- /dev/null
@@ -0,0 +1,51 @@
+#!/bin/rc -e
+rfork en
+. /sys/lib/git/common.rc
+
+gitup
+
+flagfmt='a:pushall, b:branch branch, f:force, d:debug,
+         r:remove remove, u:upstream upstream' args=''
+eval `''{aux/getflags $*} || exec aux/usage
+if(! ~ $#* 0)
+       exec aux/usage
+
+if(~ $pushall 1)
+       branch=`$nl{cd .git/refs/heads && walk -f}
+if(~ $#branch 0)
+       branch=`{git/branch}
+if(~ $#branch 0)
+       die 'no branches'
+if(~ $force 1)
+       force=-f
+if(~ $debug 1)
+       debug='-d'
+
+if(~ $#upstream 0)
+       upstream=origin
+
+remotes=`$nl{git/conf -a 'remote "'$upstream'".url'}
+if(~ $#remotes 0)
+       remotes=$upstream
+branch=-b^$branch
+if(! ~ $#remove 0)
+       remove=-r^$remove
+for(remote in $remotes){
+       updates=`$nl{git/send $debug $force $branch $remove $remote || die $status}
+       for(ln in $updates){
+               u=`{echo $ln}
+               refpath=`{echo $u(2) | subst '^refs/heads/' '.git/refs/remotes/'$upstream'/'}
+               switch($u(1)){
+               case update;
+                       mkdir -p `{basename -d $refpath}
+                       echo $u(4) > $refpath
+                       echo $u(2)^':' $u(3) '=>' $u(4)
+               case delete;
+                       echo $u(2)^': removed'
+                       rm -f $refpath
+               case uptodate;
+                       echo $u(2)^': up to date'
+               }
+       }
+}
+exit ''
diff --git a/sys/src/cmd/git/query.c b/sys/src/cmd/git/query.c
new file mode 100644 (file)
index 0000000..5f1330d
--- /dev/null
@@ -0,0 +1,196 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+#pragma        varargck        type    "P"     void
+
+int fullpath;
+int changes;
+int reverse;
+char *path[128];
+int npath;
+
+int
+Pfmt(Fmt *f)
+{
+       int i, n;
+
+       n = 0;
+       for(i = 0; i < npath; i++)
+               n += fmtprint(f, "%s/", path[i]);
+       return n;
+}
+
+void
+showdir(Hash dh, char *dname, char m)
+{
+       Dirent *p, *e;
+       Object *d;
+
+
+       path[npath++] = dname;
+       if((d = readobject(dh)) == nil)
+               sysfatal("bad hash %H", dh);
+       assert(d->type == GTree);
+       p = d->tree->ent;
+       e = p + d->tree->nent;
+       for(; p != e; p++){
+               if(p->ismod)
+                       continue;
+               if(p->mode & DMDIR)
+                       showdir(p->h, p->name, m);
+               else
+                       print("%c %P%s\n", m, p->name);
+       }
+       print("%c %P\n", m);
+       unref(d);
+       npath--;
+}
+
+void
+show(Dirent *e, char m)
+{
+       if(e->mode & DMDIR)
+               showdir(e->h, e->name, m);
+       else
+               print("%c %P%s\n", m, e->name);
+}
+
+void
+difftrees(Object *a, Object *b)
+{
+       Dirent *ap, *bp, *ae, *be;
+       int c;
+
+       ap = ae = nil;
+       bp = be = nil;
+       if(a != nil){
+               if(a->type != GTree)
+                       return;
+               ap = a->tree->ent;
+               ae = ap + a->tree->nent;
+       }
+       if(b != nil){
+               if(b->type != GTree)
+                       return;
+               bp = b->tree->ent;
+               be = bp + b->tree->nent;
+       }
+       while(ap != ae && bp != be){
+               c = strcmp(ap->name, bp->name);
+               if(c == 0){
+                       if(ap->mode == bp->mode && hasheq(&ap->h, &bp->h))
+                               goto next;
+                       if(ap->mode != bp->mode)
+                               print("! %P%s\n", ap->name);
+                       else if(!(ap->mode & DMDIR) || !(bp->mode & DMDIR))
+                               print("@ %P%s\n", ap->name);
+                       if((ap->mode & DMDIR) && (bp->mode & DMDIR)){
+                               if(npath >= nelem(path))
+                                       sysfatal("path too deep");
+                               path[npath++] = ap->name;
+                               if((a = readobject(ap->h)) == nil)
+                                       sysfatal("bad hash %H", ap->h);
+                               if((b = readobject(bp->h)) == nil)
+                                       sysfatal("bad hash %H", bp->h);
+                               difftrees(a, b);
+                               unref(a);
+                               unref(b);
+                               npath--;
+                       }
+next:
+                       ap++;
+                       bp++;
+               }else if(c < 0) {
+                       show(ap, '-');
+                       ap++;
+               }else if(c > 0){
+                       show(bp, '+');
+                       bp++;
+               }
+       }
+       for(; ap != ae; ap++)
+               show(ap, '-');
+       for(; bp != be; bp++)
+               show(bp, '+');
+}
+
+void
+diffcommits(Hash ah, Hash bh)
+{
+       Object *a, *b, *at, *bt;
+
+       at = nil;
+       bt = nil;
+       if(!hasheq(&ah, &Zhash) && (a = readobject(ah)) != nil){
+               if(a->type != GCommit)
+                       sysfatal("not commit: %H", ah);
+               if((at = readobject(a->commit->tree)) == nil)
+                       sysfatal("bad hash %H", a->commit->tree);
+               unref(a);
+       }
+       if(!hasheq(&bh, &Zhash) && (b = readobject(bh)) != nil){
+               if(b->type != GCommit)
+                       sysfatal("not commit: %H", ah);
+               if((bt = readobject(b->commit->tree)) == nil)
+                       sysfatal("bad hash %H", b->commit->tree);
+               unref(b);
+       }
+       difftrees(at, bt);
+       unref(at);
+       unref(bt);
+}
+
+void
+usage(void)
+{
+       fprint(2, "usage: %s [-pcr] query\n", argv0);
+       exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+       int i, j, n;
+       Hash *h;
+       char *p, *e, *s;
+       char query[2048], repo[512];
+
+       ARGBEGIN{
+       case 'p':       fullpath++;     break;
+       case 'c':       changes++;      break;
+       case 'r':       reverse ^= 1;   break;
+       default:        usage();        break;
+       }ARGEND;
+
+       gitinit();
+       fmtinstall('P', Pfmt);
+
+       if(argc == 0)
+               usage();
+       if(findrepo(repo, sizeof(repo)) == -1)
+               sysfatal("find root: %r");
+       if(chdir(repo) == -1)
+               sysfatal("chdir: %r");
+       s = "";
+       p = query;
+       e = query + nelem(query);
+       for(i = 0; i < argc; i++){
+               p = seprint(p, e, "%s%s", s, argv[i]);
+               s = " ";
+       }
+       if((n = resolverefs(&h, query)) == -1)
+               sysfatal("resolve: %r");
+       if(changes){
+               if(n != 2)
+                       sysfatal("diff: need 2 commits, got %d", n);
+               diffcommits(h[0], h[1]);
+       }else{
+               p = (fullpath ? "/mnt/git/object/" : "");
+               for(j = 0; j < n; j++)
+                       print("%s%H\n", p, h[reverse ? n - 1 - j : j]);
+       }
+       exits(nil);
+}
+
diff --git a/sys/src/cmd/git/rebase b/sys/src/cmd/git/rebase
new file mode 100755 (executable)
index 0000000..9ceb39f
--- /dev/null
@@ -0,0 +1,92 @@
+#!/bin/rc
+
+. /sys/lib/git/common.rc
+gitup
+
+flagfmt='a:abort, r:resume, i:interactive'; args='onto'
+eval `''{aux/getflags $*} || exec aux/usage
+
+tmp=_rebase.working
+if(! git/walk -q)
+       die dirty working tree
+if(~ $#abort 1){
+       if(! test -f .git/rebase.todo)
+               die no rebase to abort
+       src=`{cat .git/rebase.src}
+       rm -f .git/rebase.^(src todo)
+       git/branch $src
+       git/branch -d $tmp
+       exit
+}
+if(test -f .git/rebase.todo){
+       if(~ $#resume 0)
+               die rebase in progress
+       if(! ~ $#* 0)
+               exec aux/usage
+       src=`{cat .git/rebase.src}
+}
+if not{
+       if(! ~ $#* 1)
+               exec aux/usage
+       src=`{git/branch}
+       dst=`{git/query $1}
+       echo $src > .git/rebase.src
+       git/log -se $dst' '$src' @ .. '$src | sed 's/^/pick /' >.git/rebase.todo
+       if(! ~ $#interactive 0){
+               giteditor=`{git/conf core.editor}
+               if(~ $#editor 0)
+                       editor=$giteditor
+               if(~ $#editor 0)
+                       editor=hold
+               $editor .git/rebase.todo
+       }
+       git/branch -nb $dst $tmp
+}
+todo=`$nl{cat .git/rebase.todo}
+
+fn sigexit {
+       s=$status
+       if(!)
+               echo 'fix and git/rebase -r'
+       >.git/rebase.todo for(i in $todo)
+               echo $i
+       status=$s
+}
+
+flag e +
+
+while(! ~ $#todo 0){
+       item=`{echo $todo(1)}
+       todo=$todo(2-)
+       echo $item
+       c=$item(2)
+       switch($item(1)){
+       case p pick
+               git/export $c | git/import
+       case r reword
+               git/export $c | git/import
+               git/commit -re
+       case e edit
+               git/export $c | git/import
+               echo 'stopped for edit, resume with git/rebase -r'
+               exit
+       case s squash
+               git/export $c | git/import -n
+               msg=`''{cat /mnt/git/HEAD/msg; echo; cat /mnt/git/object/$c/msg}
+               git/commit -rem $msg .
+       case f fixup
+               git/export $c | git/import -n
+               git/commit -r .
+       case b break
+               echo 'stopped, resume with git/rebase -r'
+               exit
+       case '#'* ''
+       case *
+               die 'unknown command '''^$item(1)^''''
+       }
+}
+
+fn sigexit
+git/branch -nb $tmp $src
+git/branch -d $tmp
+rm .git/rebase.todo .git/rebase.src
diff --git a/sys/src/cmd/git/ref.c b/sys/src/cmd/git/ref.c
new file mode 100644 (file)
index 0000000..80d79bd
--- /dev/null
@@ -0,0 +1,677 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+typedef struct Eval    Eval;
+typedef struct XObject XObject;
+typedef struct Objq    Objq;
+
+enum {
+       Blank,
+       Keep,
+       Drop,
+};
+
+struct Eval {
+       char    *str;
+       char    *p;
+       Object  **stk;
+       int     nstk;
+       int     stksz;
+};
+
+struct XObject {
+       Object  *obj;
+       Object  *mark;
+       XObject *queue;
+       XObject *next;
+};
+
+struct Objq {
+       Objq    *next;
+       Object  *o;
+       int     color;
+};
+
+static Object zcommit = {
+       .type=GCommit
+};
+
+void
+eatspace(Eval *ev)
+{
+       while(isspace(ev->p[0]))
+               ev->p++;
+}
+
+int
+objdatecmp(void *pa, void *pb)
+{
+       Object *a, *b;
+       int r;
+
+       a = readobject((*(Object**)pa)->hash);
+       b = readobject((*(Object**)pb)->hash);
+       assert(a->type == GCommit && b->type == GCommit);
+       if(a->commit->mtime == b->commit->mtime)
+               r = 0;
+       else if(a->commit->mtime < b->commit->mtime)
+               r = -1;
+       else
+               r = 1;
+       unref(a);
+       unref(b);
+       return r;
+}
+
+void
+push(Eval *ev, Object *o)
+{
+       if(ev->nstk == ev->stksz){
+               ev->stksz = 2*ev->stksz + 1;
+               ev->stk = erealloc(ev->stk, ev->stksz*sizeof(Object*));
+       }
+       ev->stk[ev->nstk++] = o;
+}
+
+Object*
+pop(Eval *ev)
+{
+       if(ev->nstk == 0)
+               sysfatal("stack underflow");
+       return ev->stk[--ev->nstk];
+}
+
+Object*
+peek(Eval *ev)
+{
+       if(ev->nstk == 0)
+               sysfatal("stack underflow");
+       return ev->stk[ev->nstk - 1];
+}
+
+int
+isword(char e)
+{
+       return isalnum(e) || e == '/' || e == '-' || e == '_' || e == '.';
+}
+
+int
+word(Eval *ev, char *b, int nb)
+{
+       char *p, *e;
+       int n;
+
+       p = ev->p;
+       for(e = p; isword(*e) && strncmp(e, "..", 2) != 0; e++)
+               /* nothing */;
+       /* 1 for nul terminator */
+       n = e - p + 1;
+       if(n >= nb)
+               n = nb;
+       snprint(b, n, "%s", p);
+       ev->p = e;
+       return n > 0;
+}
+
+int
+take(Eval *ev, char *m)
+{
+       int l;
+
+       l = strlen(m);
+       if(strncmp(ev->p, m, l) != 0)
+               return 0;
+       ev->p += l;
+       return 1;
+}
+
+XObject*
+hnode(XObject *ht[], Object *o)
+{
+       XObject *h;
+       int     hh;
+
+       hh = o->hash.h[0] & 0xff;
+       for(h = ht[hh]; h; h = h->next)
+               if(hasheq(&o->hash, &h->obj->hash))
+                       return h;
+
+       h = emalloc(sizeof(*h));
+       h->obj = o;
+       h->mark = nil;
+       h->queue = nil;
+       h->next = ht[hh];
+       ht[hh] = h;
+       return h;
+}
+
+Object*
+ancestor(Object *a, Object *b)
+{
+       Object *o, *p, *r;
+       XObject *ht[256];
+       XObject *h, *q, *q1, *q2;
+       int i;
+
+       if(a == b)
+               return a;
+       if(a == nil || b == nil)
+               return nil;
+       r = nil;
+       memset(ht, 0, sizeof(ht));
+       q1 = nil;
+
+       h = hnode(ht, a);
+       h->mark = a;
+       h->queue = q1;
+       q1 = h;
+
+       h = hnode(ht, b);
+       h->mark = b;
+       h->queue = q1;
+       q1 = h;
+
+       while(1){
+               q2 = nil;
+               while(q = q1){
+                       q1 = q->queue;
+                       q->queue = nil;
+                       o = q->obj;
+                       for(i = 0; i < o->commit->nparent; i++){
+                               p = readobject(o->commit->parent[i]);
+                               if(p == nil)
+                                       goto err;
+                               h = hnode(ht, p);
+                               if(h->mark != nil){
+                                       if(h->mark != q->mark){
+                                               r = h->obj;
+                                               goto done;
+                                       }
+                               } else {
+                                       h->mark = q->mark;
+                                       h->queue = q2;
+                                       q2 = h;
+                               }
+                       }
+               }
+               if(q2 == nil){
+err:
+                       werrstr("no common ancestor");
+                       break;
+               }
+               q1 = q2;
+       }
+done:
+       for(i=0; i<nelem(ht); i++){
+               while(h = ht[i]){
+                       ht[i] = h->next;
+                       free(h);
+               }
+       }
+       return r;
+}
+
+int
+lca(Eval *ev)
+{
+       Object *a, *b, *o;
+
+       if(ev->nstk < 2){
+               werrstr("ancestor needs 2 objects");
+               return -1;
+       }
+       a = pop(ev);
+       b = pop(ev);
+       o = ancestor(a, b);
+       if(o == nil)
+               return -1;
+       push(ev, o);
+       return 0;
+}
+
+static int
+repaint(Objset *keep, Objset *drop, Object *o)
+{
+       Object *p;
+       int i;
+
+       if(!oshas(keep, o->hash) && !oshas(drop, o->hash)){
+               dprint(2, "repaint: blank => drop %H\n", o->hash);
+               osadd(drop, o);
+               return 0;
+       }
+       if(oshas(keep, o->hash))
+               dprint(2, "repaint: keep => drop %H\n", o->hash);
+       osadd(drop, o);
+       for(i = 0; i < o->commit->nparent; i++){
+               if((p = readobject(o->commit->parent[i])) == nil)
+                       return -1;
+               if(repaint(keep, drop, p) == -1)
+                       return -1;
+               unref(p);
+       }
+       return 0;
+}
+
+int
+findtwixt(Hash *head, int nhead, Hash *tail, int ntail, Object ***res, int *nres)
+{
+       Objq *q, *e, *n, **p;
+       Objset keep, drop;
+       Object *o, *c;
+       int i, ncolor;
+
+       e = nil;
+       q = nil;
+       p = &q;
+       osinit(&keep);
+       osinit(&drop);
+       for(i = 0; i < nhead; i++){
+               if(hasheq(&head[i], &Zhash))
+                       continue;
+               if((o = readobject(head[i])) == nil){
+                       fprint(2, "warning: %H does not point at commit\n", o->hash);
+                       werrstr("read head %H: %r", head[i]);
+                       return -1;
+               }
+               if(o->type != GCommit){
+                       fprint(2, "warning: %H does not point at commit\n", o->hash);
+                       unref(o);
+                       continue;
+               }
+               dprint(1, "twixt init: keep %H\n", o->hash);
+               e = emalloc(sizeof(Objq));
+               e->o = o;
+               e->color = Keep;
+               *p = e;
+               p = &e->next;
+               unref(o);
+       }               
+       for(i = 0; i < ntail; i++){
+               if(hasheq(&tail[i], &Zhash))
+                       continue;
+               if((o = readobject(tail[i])) == nil){
+                       fprint(2, "warning: %H does not point at commit\n", o->hash);
+                       werrstr("read tail %H: %r", tail[i]);
+                       return -1;
+               }
+               if(o->type != GCommit){
+                       unref(o);
+                       continue;
+               }
+               dprint(1, "init: drop %H\n", o->hash);
+               e = emalloc(sizeof(Objq));
+               e->o = o;
+               e->color = Drop;
+               *p = e;
+               p = &e->next;
+               unref(o);
+       }
+
+       dprint(1, "finding twixt commits\n");
+       while(q != nil){
+               if(oshas(&drop, q->o->hash))
+                       ncolor = Drop;
+               else if(oshas(&keep, q->o->hash))
+                       ncolor = Keep;
+               else
+                       ncolor = Blank;
+               if(ncolor == Drop || ncolor == Keep && q->color == Keep)
+                       goto next;
+               if(ncolor == Keep && q->color == Drop){
+                       if(repaint(&keep, &drop, q->o) == -1)
+                               goto error;
+               }else if (ncolor == Blank) {
+                       dprint(2, "visit: %s %H\n", q->color == Keep ? "keep" : "drop", q->o->hash);
+                       if(q->color == Keep)
+                               osadd(&keep, q->o);
+                       else
+                               osadd(&drop, q->o);
+                       for(i = 0; i < q->o->commit->nparent; i++){
+                               if((c = readobject(q->o->commit->parent[i])) == nil)
+                                       goto error;
+                               if(c->type != GCommit){
+                                       fprint(2, "warning: %H does not point at commit\n", c->hash);
+                                       unref(c);
+                                       continue;
+                               }
+                               dprint(2, "enqueue: %s %H\n", q->color == Keep ? "keep" : "drop", c->hash);
+                               n = emalloc(sizeof(Objq));
+                               n->color = q->color;
+                               n->next = nil;
+                               n->o = c;
+                               e->next = n;
+                               e = n;
+                               unref(c);
+                       }
+               }else{
+                       sysfatal("oops");
+               }
+next:
+               n = q->next;
+               free(q);
+               q = n;
+       }
+       *res = eamalloc(keep.nobj, sizeof(Object*));
+       *nres = 0;
+       for(i = 0; i < keep.sz; i++){
+               if(keep.obj[i] != nil && !oshas(&drop, keep.obj[i]->hash)){
+                       (*res)[*nres] = keep.obj[i];
+                       (*nres)++;
+               }
+       }
+       osclear(&keep);
+       osclear(&drop);
+       return 0;
+error:
+       for(; q != nil; q = n) {
+               n = q->next;
+               free(q);
+       }
+       return -1;
+}
+
+static int
+parent(Eval *ev)
+{
+       Object *o, *p;
+
+       o = pop(ev);
+       /* Special case: first commit has no parent. */
+       if(o->commit->nparent == 0)
+               p = emptydir();
+       else if ((p = readobject(o->commit->parent[0])) == nil){
+               werrstr("no parent for %H", o->hash);
+               return -1;
+       }
+               
+       push(ev, p);
+       return 0;
+}
+
+static int
+unwind(Eval *ev, Object **obj, int *idx, int nobj, Object **p, Objset *set, int keep)
+{
+       int i;
+
+       for(i = nobj; i >= 0; i--){
+               idx[i]++;
+               if(keep && !oshas(set, obj[i]->hash)){
+                       push(ev, obj[i]);
+                       osadd(set, obj[i]);
+               }else{
+                       osadd(set, obj[i]);
+               }
+               if(idx[i] < obj[i]->commit->nparent){
+                       *p = obj[i];
+                       return i;
+               }
+               unref(obj[i]);
+       }
+       return -1;
+}
+
+static int
+range(Eval *ev)
+{
+       Object *a, *b, *p, *q, **all;
+       int nall, *idx, mark;
+       Objset keep, skip;
+
+       b = pop(ev);
+       a = pop(ev);
+       if(hasheq(&b->hash, &Zhash))
+               b = &zcommit;
+       if(hasheq(&a->hash, &Zhash))
+               a = &zcommit;
+       if(a->type != GCommit || b->type != GCommit){
+               werrstr("non-commit object in range");
+               return -1;
+       }
+
+       p = b;
+       all = nil;
+       idx = nil;
+       nall = 0;
+       mark = ev->nstk;
+       osinit(&keep);
+       osinit(&skip);
+       osadd(&keep, a);
+       while(1){
+               all = earealloc(all, (nall + 1), sizeof(Object*));
+               idx = earealloc(idx, (nall + 1), sizeof(int));
+               all[nall] = p;
+               idx[nall] = 0;
+               if(p == a || p->commit->nparent == 0 && a == &zcommit){
+                       if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1)
+                               break;
+               }else if(p->commit->nparent == 0){
+                       if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1)
+                               break;
+               }else if(oshas(&keep, p->hash)){
+                       if((nall = unwind(ev, all, idx, nall, &p, &keep, 1)) == -1)
+                               break;
+               }else if(oshas(&skip, p->hash))
+                       if((nall = unwind(ev, all, idx, nall, &p, &skip, 0)) == -1)
+                               break;
+               if(p->commit->nparent == 0)
+                       break;
+               if((q = readobject(p->commit->parent[idx[nall]])) == nil){
+                       werrstr("bad commit %H", p->commit->parent[idx[nall]]);
+                       goto error;
+               }
+               if(q->type != GCommit){
+                       werrstr("not commit: %H", q->hash);
+                       goto error;
+               }
+               p = q;
+               nall++;
+       }
+       free(all);
+       qsort(ev->stk + mark, ev->nstk - mark, sizeof(Object*), objdatecmp);
+       return 0;
+error:
+       free(all);
+       return -1;
+}
+
+int
+readref(Hash *h, char *ref)
+{
+       static char *try[] = {"", "refs/", "refs/heads/", "refs/remotes/", "refs/tags/", nil};
+       char buf[256], s[256], **pfx;
+       int r, f, n;
+
+       /* TODO: support hash prefixes */
+       if((r = hparse(h, ref)) != -1)
+               return r;
+       if(strcmp(ref, "HEAD") == 0){
+               snprint(buf, sizeof(buf), ".git/HEAD");
+               if((f = open(buf, OREAD)) == -1)
+                       return -1;
+               if((n = readn(f, s, sizeof(s) - 1))== -1)
+                       return -1;
+               s[n] = 0;
+               strip(s);
+               r = hparse(h, s);
+               goto found;
+       }
+       for(pfx = try; *pfx; pfx++){
+               snprint(buf, sizeof(buf), ".git/%s%s", *pfx, ref);
+               if((f = open(buf, OREAD)) == -1)
+                       continue;
+               if((n = readn(f, s, sizeof(s) - 1)) == -1)
+                       continue;
+               s[n] = 0;
+               strip(s);
+               r = hparse(h, s);
+               close(f);
+               goto found;
+       }
+       return -1;
+
+found:
+       if(r == -1 && strstr(s, "ref: ") == s)
+               r = readref(h, s + strlen("ref: "));
+       return r;
+}
+
+int
+evalpostfix(Eval *ev)
+{
+       char name[256];
+       Object *o;
+       Hash h;
+
+       eatspace(ev);
+       if(!word(ev, name, sizeof(name))){
+               werrstr("expected name in expression");
+               return -1;
+       }
+       if(readref(&h, name) == -1){
+               werrstr("invalid ref %s", name);
+               return -1;
+       }
+       if(hasheq(&h, &Zhash))
+               o = &zcommit;
+       else if((o = readobject(h)) == nil){
+               werrstr("invalid ref %s (hash %H)", name, h);
+               return -1;
+       }
+       push(ev, o);
+
+       while(1){
+               eatspace(ev);
+               switch(ev->p[0]){
+               case '^':
+               case '~':
+                       ev->p++;
+                       if(parent(ev) == -1)
+                               return -1;
+                       break;
+               case '@':
+                       ev->p++;
+                       if(lca(ev) == -1)
+                               return -1;
+                       break;
+               default:
+                       goto done;
+                       break;
+               }       
+       }
+done:
+       return 0;
+}
+
+int
+evalexpr(Eval *ev, char *ref)
+{
+       memset(ev, 0, sizeof(*ev));
+       ev->str = ref;
+       ev->p = ref;
+
+       while(1){
+               if(evalpostfix(ev) == -1)
+                       return -1;
+               if(ev->p[0] == '\0')
+                       return 0;
+               else if(take(ev, ":") || take(ev, "..")){
+                       if(evalpostfix(ev) == -1)
+                               return -1;
+                       if(ev->p[0] != '\0'){
+                               werrstr("junk at end of expression");
+                               return -1;
+                       }
+                       return range(ev);
+               }
+       }
+}
+
+int
+resolverefs(Hash **r, char *ref)
+{
+       Eval ev;
+       Hash *h;
+       int i;
+
+       if(evalexpr(&ev, ref) == -1){
+               free(ev.stk);
+               return -1;
+       }
+       h = eamalloc(ev.nstk, sizeof(Hash));
+       for(i = 0; i < ev.nstk; i++)
+               h[i] = ev.stk[i]->hash;
+       *r = h;
+       free(ev.stk);
+       return ev.nstk;
+}
+
+int
+resolveref(Hash *r, char *ref)
+{
+       Eval ev;
+
+       if(evalexpr(&ev, ref) == -1){
+               free(ev.stk);
+               return -1;
+       }
+       if(ev.nstk != 1){
+               werrstr("ambiguous ref expr");
+               free(ev.stk);
+               return -1;
+       }
+       *r = ev.stk[0]->hash;
+       free(ev.stk);
+       return 0;
+}
+
+int
+readrefdir(Hash **refs, char ***names, int *nrefs, char *dpath, char *dname)
+{
+       Dir *d, *e, *dir;
+       char *path, *name, *sep;
+       int ndir;
+
+       if((ndir = slurpdir(dpath, &dir)) == -1)
+               return -1;
+       sep = (*dname == '\0') ? "" : "/";
+       e = dir + ndir;
+       for(d = dir; d != e; d++){
+               path = smprint("%s/%s", dpath, d->name);
+               name = smprint("%s%s%s", dname, sep, d->name);
+               if(d->mode & DMDIR) {
+                       if(readrefdir(refs, names, nrefs, path, name) == -1)
+                               goto noref;
+               }else{
+                       *refs = erealloc(*refs, (*nrefs + 1)*sizeof(Hash));
+                       *names = erealloc(*names, (*nrefs + 1)*sizeof(char*));
+                       if(resolveref(&(*refs)[*nrefs], name) == -1)
+                               goto noref;
+                       (*names)[*nrefs] = name;
+                       *nrefs += 1;
+                       goto next;
+               }
+noref:         free(name);
+next:          free(path);
+       }
+       free(dir);
+       return 0;
+}
+
+int
+listrefs(Hash **refs, char ***names)
+{
+       int nrefs;
+
+       *refs = nil;
+       *names = nil;
+       nrefs = 0;
+       if(readrefdir(refs, names, &nrefs, ".git/refs", "") == -1){
+               free(*refs);
+               return -1;
+       }
+       return nrefs;
+}
diff --git a/sys/src/cmd/git/repack.c b/sys/src/cmd/git/repack.c
new file mode 100644 (file)
index 0000000..601231b
--- /dev/null
@@ -0,0 +1,85 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+#define TMPPATH(suff) (".git/objects/pack/repack."suff)
+
+int
+cleanup(Hash h)
+{
+       char newpfx[42], dpath[256], fpath[256];
+       int i, j, nd;
+       Dir *d;
+
+       snprint(newpfx, sizeof(newpfx), "%H.", h);
+       for(i = 0; i < 256; i++){
+               snprint(dpath, sizeof(dpath), ".git/objects/%02x", i);
+               if((nd = slurpdir(dpath, &d)) == -1)
+                       continue;
+               for(j = 0; j < nd; j++){
+                       snprint(fpath, sizeof(fpath), ".git/objects/%02x/%s", i, d[j].name);
+                       remove(fpath);
+               }
+               remove(dpath);
+               free(d);
+       }
+       snprint(dpath, sizeof(dpath), ".git/objects/pack");
+       if((nd = slurpdir(dpath, &d)) == -1)
+               return -1;
+       for(i = 0; i < nd; i++){
+               if(strncmp(d[i].name, newpfx, strlen(newpfx)) == 0)
+                       continue;
+               snprint(fpath, sizeof(fpath), ".git/objects/pack/%s", d[i].name);
+               remove(fpath);
+       }
+       return 0;
+}
+
+void
+usage(void)
+{
+       fprint(2, "usage: %s [-d]\n", argv0);
+       exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+       char path[128], **names;
+       int fd, nrefs;
+       Hash *refs, h;
+       Dir rn;
+
+       ARGBEGIN{
+       case 'd':
+               chattygit++;
+               break;
+       default:
+               usage();
+       }ARGEND;
+
+       gitinit();
+       refs = nil;
+       if((nrefs = listrefs(&refs, &names)) == -1)
+               sysfatal("load refs: %r");
+       if((fd = create(TMPPATH("pack.tmp"), OWRITE, 0644)) == -1)
+               sysfatal("open %s: %r", TMPPATH("pack.tmp"));
+       if(writepack(fd, refs, nrefs, nil, 0, &h) == -1)
+               sysfatal("writepack: %r");
+       if(indexpack(TMPPATH("pack.tmp"), TMPPATH("idx.tmp"), h) == -1)
+               sysfatal("indexpack: %r");
+       close(fd);
+
+       nulldir(&rn);
+       rn.name = path;
+       snprint(path, sizeof(path), "%H.pack", h);
+       if(dirwstat(TMPPATH("pack.tmp"), &rn) == -1)
+               sysfatal("rename pack: %r");
+       snprint(path, sizeof(path), "%H.idx", h);
+       if(dirwstat(TMPPATH("idx.tmp"), &rn) == -1)
+               sysfatal("rename pack: %r");
+       if(cleanup(h) == -1)
+               sysfatal("cleanup: %r");
+       exits(nil);
+}
diff --git a/sys/src/cmd/git/revert b/sys/src/cmd/git/revert
new file mode 100644 (file)
index 0000000..af02f5c
--- /dev/null
@@ -0,0 +1,19 @@
+#!/bin/rc
+rfork e
+. /sys/lib/git/common.rc
+
+gitup
+
+flagfmt='c:query query' args='file ...'
+eval `''{aux/getflags $*} || exec aux/usage
+
+commit=/mnt/git/HEAD
+if(~ $#query 1)
+       commit=`{git/query -p $query}
+
+for(f in `$nl{cd $commit/tree/ && walk -f ./$gitrel/$*}){
+       mkdir -p `{basename -d $f}
+       cp -- $commit/tree/$f $f
+       git/add $f
+}
+exit ''
diff --git a/sys/src/cmd/git/rm b/sys/src/cmd/git/rm
new file mode 100755 (executable)
index 0000000..deaf3ae
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/rc -e
+
+exec git/add -r $*
diff --git a/sys/src/cmd/git/save.c b/sys/src/cmd/git/save.c
new file mode 100644 (file)
index 0000000..69e5c2b
--- /dev/null
@@ -0,0 +1,401 @@
+#include <u.h>
+#include <libc.h>
+#include "git.h"
+
+typedef struct Objbuf Objbuf;
+struct Objbuf {
+       int off;
+       char *hdr;
+       int nhdr;
+       char *dat;
+       int ndat;
+};
+enum {
+       Maxparents = 16,
+};
+
+int
+gitmode(int m)
+{
+       if(m & DMDIR)           /* directory */
+               return 0040000;
+       else if(m & 0111)       /* executable */
+               return 0100755;
+       else if(m != 0)         /* regular */
+               return 0100644;
+       else                    /* symlink */
+               return 0120000;
+}
+
+int
+entcmp(void *pa, void *pb)
+{
+       char abuf[256], bbuf[256], *ae, *be;
+       Dirent *a, *b;
+
+       a = pa;
+       b = pb;
+       /*
+        * If the files have the same name, they're equal.
+        * Otherwise, If they're trees, they sort as thoug
+        * there was a trailing slash.
+        *
+        * Wat.
+        */
+       if(strcmp(a->name, b->name) == 0)
+               return 0;
+
+       ae = seprint(abuf, abuf + sizeof(abuf) - 1, a->name);
+       be = seprint(bbuf, bbuf + sizeof(bbuf) - 1, b->name);
+       if(a->mode & DMDIR)
+               *ae = '/';
+       if(b->mode & DMDIR)
+               *be = '/';
+       return strcmp(abuf, bbuf);
+}
+
+static int
+bwrite(void *p, void *buf, int nbuf)
+{
+       return Bwrite(p, buf, nbuf);
+}
+
+static int
+objbytes(void *p, void *buf, int nbuf)
+{
+       Objbuf *b;
+       int r, n, o;
+       char *s;
+
+       b = p;
+       n = 0;
+       if(b->off < b->nhdr){
+               r = b->nhdr - b->off;
+               r = (nbuf < r) ? nbuf : r;
+               memcpy(buf, b->hdr, r);
+               b->off += r;
+               nbuf -= r;
+               n += r;
+       }
+       if(b->off < b->ndat + b->nhdr){
+               s = buf;
+               o = b->off - b->nhdr;
+               r = b->ndat - o;
+               r = (nbuf < r) ? nbuf : r;
+               memcpy(s + n, b->dat + o, r);
+               b->off += r;
+               n += r;
+       }
+       return n;
+}
+
+void
+writeobj(Hash *h, char *hdr, int nhdr, char *dat, int ndat)
+{
+       Objbuf b = {.off=0, .hdr=hdr, .nhdr=nhdr, .dat=dat, .ndat=ndat};
+       char s[64], o[256];
+       SHA1state *st;
+       Biobuf *f;
+       int fd;
+
+       st = sha1((uchar*)hdr, nhdr, nil, nil);
+       st = sha1((uchar*)dat, ndat, nil, st);
+       sha1(nil, 0, h->h, st);
+
+       snprint(s, sizeof(s), "%H", *h);
+       fd = create(".git/objects", OREAD, DMDIR|0755);
+       close(fd);
+       snprint(o, sizeof(o), ".git/objects/%c%c", s[0], s[1]);
+       fd = create(o, OREAD, DMDIR | 0755);
+       close(fd);
+       snprint(o, sizeof(o), ".git/objects/%c%c/%s", s[0], s[1], s + 2);
+       if(readobject(*h) == nil){
+               if((f = Bopen(o, OWRITE)) == nil)
+                       sysfatal("could not open %s: %r", o);
+               if(deflatezlib(f, bwrite, &b, objbytes, 9, 0) == -1)
+                       sysfatal("could not write %s: %r", o);
+               Bterm(f);
+       }
+}
+
+int
+writetree(Dirent *ent, int nent, Hash *h)
+{
+       char *t, *txt, *etxt, hdr[128];
+       int nhdr, n;
+       Dirent *d, *p;
+
+       t = emalloc((16+256+20) * nent);
+       txt = t;
+       etxt = t + (16+256+20) * nent;
+
+       /* sqeeze out deleted entries */
+       n = 0;
+       p = ent;
+       for(d = ent; d != ent + nent; d++)
+               if(d->name)
+                       p[n++] = *d;
+       nent = n;
+
+       qsort(ent, nent, sizeof(Dirent), entcmp);
+       for(d = ent; d != ent + nent; d++){
+               if(strlen(d->name) >= 255)
+                       sysfatal("overly long filename: %s", d->name);
+               t = seprint(t, etxt, "%o %s", gitmode(d->mode), d->name) + 1;
+               memcpy(t, d->h.h, sizeof(d->h.h));
+               t += sizeof(d->h.h);
+       }
+       nhdr = snprint(hdr, sizeof(hdr), "%T %lld", GTree, (vlong)(t - txt)) + 1;
+       writeobj(h, hdr, nhdr, txt, t - txt);
+       free(txt);
+       return nent;
+}
+
+void
+blobify(Dir *d, char *path, int *mode, Hash *bh)
+{
+       char h[64], *buf;
+       int f, nh;
+
+       if((d->mode & DMDIR) != 0)
+               sysfatal("not file: %s", path);
+       *mode = d->mode;
+       nh = snprint(h, sizeof(h), "%T %lld", GBlob, d->length) + 1;
+       if((f = open(path, OREAD)) == -1)
+               sysfatal("could not open %s: %r", path);
+       buf = emalloc(d->length);
+       if(readn(f, buf, d->length) != d->length)
+               sysfatal("could not read blob %s: %r", path);
+       writeobj(bh, h, nh, buf, d->length);
+       free(buf);
+       close(f);
+}
+
+int
+tracked(char *path)
+{
+       char ipath[256];
+       Dir *d;
+
+       /* Explicitly removed. */
+       snprint(ipath, sizeof(ipath), ".git/index9/removed/%s", path);
+       if(strstr(cleanname(ipath), ".git/index9/removed") != ipath)
+               sysfatal("path %s leaves index", ipath);
+       d = dirstat(ipath);
+       if(d != nil && d->qid.type != QTDIR){
+               free(d);
+               return 0;
+       }
+
+       /* Explicitly added. */
+       snprint(ipath, sizeof(ipath), ".git/index9/tracked/%s", path);
+       if(strstr(cleanname(ipath), ".git/index9/tracked") != ipath)
+               sysfatal("path %s leaves index", ipath);
+       if(access(ipath, AEXIST) == 0)
+               return 1;
+
+       return 0;
+}
+
+int
+pathelt(char *buf, int nbuf, char *p, int *isdir)
+{
+       char *b;
+
+       b = buf;
+       if(*p == '/')
+               p++;
+       while(*p && *p != '/' && b != buf + nbuf)
+               *b++ = *p++;
+       *b = '\0';
+       *isdir = (*p == '/');
+       return b - buf;
+}
+
+Dirent*
+dirent(Dirent **ent, int *nent, char *name)
+{
+       Dirent *d;
+
+       for(d = *ent; d != *ent + *nent; d++)
+               if(d->name && strcmp(d->name, name) == 0)
+                       return d;
+       *nent += 1;
+       *ent = erealloc(*ent, *nent * sizeof(Dirent));
+       d = *ent + (*nent - 1);
+       memset(d, 0, sizeof(*d));
+       d->name = estrdup(name);
+       return d;
+}
+
+int
+treeify(Object *t, char **path, char **epath, int off, Hash *h)
+{
+       int r, n, ne, nsub, nent, isdir;
+       char **p, **ep;
+       char elt[256];
+       Object **sub;
+       Dirent *e, *ent;
+       Dir *d;
+
+       r = -1;
+       nsub = 0;
+       nent = t->tree->nent;
+       ent = eamalloc(nent, sizeof(*ent));
+       sub = eamalloc((epath - path), sizeof(Object*));
+       memcpy(ent, t->tree->ent, nent*sizeof(*ent));
+       for(p = path; p != epath; p = ep){
+               ne = pathelt(elt, sizeof(elt), *p + off, &isdir);
+               for(ep = p; ep != epath; ep++){
+                       if(strncmp(elt, *ep + off, ne) != 0)
+                               break;
+                       if((*ep)[off+ne] != '\0' && (*ep)[off+ne] != '/')
+                               break;
+               }
+               e = dirent(&ent, &nent, elt);
+               if(e->islink)
+                       sysfatal("symlinks may not be modified: %s", *path);
+               if(e->ismod)
+                       sysfatal("submodules may not be modified: %s", *path);
+               if(isdir){
+                       e->mode = DMDIR | 0755;
+                       sub[nsub] = readobject(e->h);
+                       if(sub[nsub] == nil || sub[nsub]->type != GTree)
+                               sub[nsub] = emptydir();
+                       /*
+                        * if after processing deletions, a tree is empty,
+                        * mark it for removal from the parent.
+                        *
+                        * Note, it is still written to the object store,
+                        * but this is fine -- and ensures that an empty
+                        * repository will continue to work.
+                        */
+                       n = treeify(sub[nsub], p, ep, off + ne + 1, &e->h);
+                       if(n == 0)
+                               e->name = nil;
+                       else if(n == -1)
+                               goto err;
+               }else{
+                       d = dirstat(*p);
+                       if(d != nil && tracked(*p))
+                               blobify(d, *p, &e->mode, &e->h);
+                       else
+                               e->name = nil;
+                       free(d);
+               }
+       }
+       if(nent == 0){
+               werrstr("%.*s: empty directory", off, *path);
+               goto err;
+       }
+
+       r = writetree(ent, nent, h);
+err:
+       free(sub);
+       return r;               
+}
+
+
+void
+mkcommit(Hash *c, char *msg, char *name, char *email, vlong date, Hash *parents, int nparents, Hash tree)
+{
+       char *s, h[64];
+       int ns, nh, i;
+       Fmt f;
+
+       fmtstrinit(&f);
+       fmtprint(&f, "tree %H\n", tree);
+       for(i = 0; i < nparents; i++)
+               fmtprint(&f, "parent %H\n", parents[i]);
+       fmtprint(&f, "author %s <%s> %lld +0000\n", name, email, date);
+       fmtprint(&f, "committer %s <%s> %lld +0000\n", name, email, date);
+       fmtprint(&f, "\n");
+       fmtprint(&f, "%s", msg);
+       s = fmtstrflush(&f);
+
+       ns = strlen(s);
+       nh = snprint(h, sizeof(h), "%T %d", GCommit, ns) + 1;
+       writeobj(c, h, nh, s, ns);
+       free(s);
+}
+
+Object*
+findroot(void)
+{
+       Object *t, *c;
+       Hash h;
+
+       if(resolveref(&h, "HEAD") == -1)
+               return emptydir();
+       if((c = readobject(h)) == nil || c->type != GCommit)
+               sysfatal("could not read HEAD %H", h);
+       if((t = readobject(c->commit->tree)) == nil)
+               sysfatal("could not read tree for commit %H", h);
+       return t;
+}
+
+void
+usage(void)
+{
+       fprint(2, "usage: %s -n name -e email -m message -d date [files...]\n", argv0);
+       exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+       Hash th, ch, parents[Maxparents];
+       char *msg, *name, *email, *dstr;
+       int i, r, nparents;
+       vlong date;
+       Object *t;
+
+       msg = nil;
+       name = nil;
+       email = nil;
+       dstr = nil;
+       date = time(nil);
+       nparents = 0;
+       gitinit();
+       ARGBEGIN{
+       case 'm':       msg = EARGF(usage());   break;
+       case 'n':       name = EARGF(usage());  break;
+       case 'e':       email = EARGF(usage()); break;
+       case 'd':       dstr = EARGF(usage());  break;
+       case 'p':
+               if(nparents >= Maxparents)
+                       sysfatal("too many parents");
+               if(resolveref(&parents[nparents++], EARGF(usage())) == -1)
+                       sysfatal("invalid parent: %r");
+               break;
+       default:
+               usage();
+       }ARGEND;
+
+       if(!msg)
+               sysfatal("missing message");
+       if(!name)
+               sysfatal("missing name");
+       if(!email)
+               sysfatal("missing email");
+       if(dstr){
+               date=strtoll(dstr, &dstr, 10);
+               if(strlen(dstr) != 0)
+                       sysfatal("could not parse date %s", dstr);
+       }
+       if(msg == nil || name == nil)
+               usage();
+       for(i = 0; i < argc; i++)
+               cleanname(argv[i]);
+
+       gitinit();
+       if(access(".git", AEXIST) != 0)
+               sysfatal("could not find git repo: %r");
+       t = findroot();
+       r = treeify(t, argv, argv + argc, 0, &th);
+       if(r == -1)
+               sysfatal("could not commit: %r\n");
+       mkcommit(&ch, msg, name, email, date, parents, nparents, th);
+       print("%H\n", ch);
+       exits(nil);
+}
diff --git a/sys/src/cmd/git/send.c b/sys/src/cmd/git/send.c
new file mode 100644 (file)
index 0000000..aa4332a
--- /dev/null
@@ -0,0 +1,272 @@
+#include <u.h>
+#include <libc.h>
+
+#include "git.h"
+
+typedef struct Capset  Capset;
+
+struct Capset {
+       int     sideband;
+       int     sideband64k;
+       int     report;
+};
+
+int sendall;
+int force;
+int nbranch;
+char **branch;
+char *removed[128];
+int nremoved;
+int npacked;
+int nsent;
+
+int
+findref(char **r, int nr, char *ref)
+{
+       int i;
+
+       for(i = 0; i < nr; i++)
+               if(strcmp(r[i], ref) == 0)
+                       return i;
+       return -1;
+}
+
+int
+readours(Hash **tailp, char ***refp)
+{
+       int nu, i, idx;
+       char *r, *pfx, **ref;
+       Hash *tail;
+
+       if(sendall)
+               return listrefs(tailp, refp);
+       nu = 0;
+       tail = eamalloc((nremoved + nbranch), sizeof(Hash));
+       ref = eamalloc((nremoved + nbranch), sizeof(char*));
+       for(i = 0; i < nbranch; i++){
+               ref[nu] = estrdup(branch[i]);
+               if(resolveref(&tail[nu], branch[i]) == -1)
+                       sysfatal("broken branch %s", branch[i]);
+               nu++;
+       }
+       for(i = 0; i < nremoved; i++){
+               pfx = "refs/heads/";
+               if(strstr(removed[i], "heads/") == removed[i])
+                       pfx = "refs/";
+               if(strstr(removed[i], "refs/heads/") == removed[i])
+                       pfx = "";
+               if((r = smprint("%s%s", pfx, removed[i])) == nil)
+                       sysfatal("smprint: %r");
+               if((idx = findref(ref, nu, r)) == -1)
+                       idx = nu++;
+               assert(idx < nremoved + nbranch);
+               memcpy(&tail[idx], &Zhash, sizeof(Hash));
+               free(r);
+       }
+       dprint(1, "nu: %d\n", nu);
+       for(i = 0; i < nu; i++)
+               dprint(1, "update: %H %s\n", tail[i], ref[i]);
+       *tailp = tail;
+       *refp = ref;
+       return nu;      
+}
+
+char *
+matchcap(char *s, char *cap, int full)
+{
+       if(strncmp(s, cap, strlen(cap)) == 0)
+               if(!full || strlen(s) == strlen(cap))
+                       return s + strlen(cap);
+       return nil;
+}
+
+void
+parsecaps(char *caps, Capset *cs)
+{
+       char *p, *n;
+
+       for(p = caps; p != nil; p = n){
+               n = strchr(p, ' ');
+               if(n != nil)
+                       *n++ = 0;
+               if(matchcap(p, "report-status", 1) != nil)
+                       cs->report = 1;
+               if(matchcap(p, "side-band", 1) != nil)
+                       cs->sideband = 1;
+               if(matchcap(p, "side-band-64k", 1) != nil)
+                       cs->sideband64k = 1;
+       }
+}
+
+int
+sendpack(Conn *c)
+{
+       int i, n, idx, nupd, nsp, send, first;
+       char buf[Pktmax], *sp[3];
+       Hash h, *theirs, *ours;
+       Object *a, *b, *p;
+       char **refs;
+       Capset cs;
+
+       first = 1;
+       memset(&cs, 0, sizeof(Capset));
+       nupd = readours(&ours, &refs);
+       theirs = eamalloc(nupd, sizeof(Hash));
+       while(1){
+               n = readpkt(c, buf, sizeof(buf));
+               if(n == -1)
+                       return -1;
+               if(n == 0)
+                       break;
+               if(first && n > strlen(buf))
+                       parsecaps(buf + strlen(buf) + 1, &cs);
+               first = 0;
+               if(strncmp(buf, "ERR ", 4) == 0)
+                       sysfatal("%s", buf + 4);
+
+               if(getfields(buf, sp, nelem(sp), 1, " \t\r\n") != 2)
+                       sysfatal("invalid ref line %.*s", utfnlen(buf, n), buf);
+               if((idx = findref(refs, nupd, sp[1])) == -1)
+                       continue;
+               if(hparse(&theirs[idx], sp[0]) == -1)
+                       sysfatal("invalid hash %s", sp[0]);
+       }
+
+       if(writephase(c) == -1)
+               return -1;
+       send = 0;
+       if(force)
+               send=1;
+       for(i = 0; i < nupd; i++){
+               a = readobject(theirs[i]);
+               b = hasheq(&ours[i], &Zhash) ? nil : readobject(ours[i]);
+               p = nil;
+               if(a != nil && b != nil)
+                       p = ancestor(a, b);
+               if(!force && !hasheq(&theirs[i], &Zhash) && (a == nil || p != a)){
+                       fprint(2, "remote has diverged\n");
+                       werrstr("force needed");
+                       flushpkt(c);
+                       return -1;
+               }
+               unref(a);
+               unref(b);
+               unref(p);
+               if(hasheq(&theirs[i], &ours[i])){
+                       print("uptodate %s\n", refs[i]);
+                       continue;
+               }
+               print("update %s %H %H\n", refs[i], theirs[i], ours[i]);
+               n = snprint(buf, sizeof(buf), "%H %H %s", theirs[i], ours[i], refs[i]);
+
+               /*
+                * Workaround for github.
+                *
+                * Github will accept the pack but fail to update the references
+                * if we don't have capabilities advertised. Report-status seems
+                * harmless to add, so we add it.
+                *
+                * Github doesn't advertise any capabilities, so we can't check
+                * for compatibility. We just need to add it blindly.
+                */
+               if(i == 0 && cs.report){
+                       buf[n++] = '\0';
+                       n += snprint(buf + n, sizeof(buf) - n, " report-status");
+               }
+               if(writepkt(c, buf, n) == -1)
+                       sysfatal("unable to send update pkt");
+               send = 1;
+       }
+       flushpkt(c);
+       if(!send){
+               fprint(2, "nothing to send\n");
+               return 0;
+       }
+
+       if(writepack(c->wfd, ours, nupd, theirs, nupd, &h) == -1)
+               return -1;
+       if(!cs.report)
+               return 0;
+
+       if(readphase(c) == -1)
+               return -1;
+       /* We asked for a status report, may as well use it. */
+       while((n = readpkt(c, buf, sizeof(buf))) > 0){
+               buf[n] = 0;
+               if(chattygit)
+                       fprint(2, "done sending pack, status %s\n", buf);
+               nsp = getfields(buf, sp, nelem(sp), 1, " \t\n\r");
+               if(nsp < 2) 
+                       continue;
+               if(nsp < 3)
+                       sp[2] = "";
+               /*
+                * Only report errors; successes will be reported by
+                * surrounding scripts.
+                */
+               if(strcmp(sp[0], "unpack") == 0 && strcmp(sp[1], "ok") != 0)
+                       fprint(2, "unpack %s\n", sp[1]);
+               else if(strcmp(sp[0], "ng") == 0)
+                       fprint(2, "failed update: %s\n", sp[1]);
+               else
+                       continue;
+               return -1;
+       }
+       return 0;
+}
+
+void
+usage(void)
+{
+       fprint(2, "usage: %s remote [reponame]\n", argv0);
+       exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+       char *br;
+       Conn c;
+
+       ARGBEGIN{
+       default:
+               usage();
+               break;
+       case 'd':
+               chattygit++;
+               break;
+       case 'f':
+               force++;
+               break;
+       case 'r':
+               if(nremoved == nelem(removed))
+                       sysfatal("too many deleted branches");
+               removed[nremoved++] = EARGF(usage());
+               break;
+       case 'a':
+               sendall++;
+               break;
+       case 'b':
+               br = EARGF(usage());
+               if(strncmp(br, "refs/heads/", strlen("refs/heads/")) == 0)
+                       br = smprint("%s", br);
+               else if(strncmp(br, "heads/", strlen("heads/")) == 0)
+                       br = smprint("refs/%s", br);
+               else
+                       br = smprint("refs/heads/%s", br);
+               branch = erealloc(branch, (nbranch + 1)*sizeof(char*));
+               branch[nbranch] = br;
+               nbranch++;
+               break;
+       }ARGEND;
+
+       gitinit();
+       if(argc != 1)
+               usage();
+       if(gitconnect(&c, argv[0], "receive") == -1)
+               sysfatal("git connect: %s: %r", argv[0]);
+       if(sendpack(&c) == -1)
+               sysfatal("send failed: %r");
+       closeconn(&c);
+       exits(nil);
+}
diff --git a/sys/src/cmd/git/serve.c b/sys/src/cmd/git/serve.c
new file mode 100644 (file)
index 0000000..4cfeadb
--- /dev/null
@@ -0,0 +1,558 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <auth.h>
+
+#include "git.h"
+
+char   *pathpfx = "/usr/git";
+char   *namespace = nil;
+int    allowwrite;
+
+int
+fmtpkt(Conn *c, char *fmt, ...)
+{
+       char pkt[Pktmax];
+       va_list ap;
+       int n;
+
+       va_start(ap, fmt);
+       n = vsnprint(pkt, sizeof(pkt), fmt, ap);
+       n = writepkt(c, pkt, n);
+       va_end(ap);
+       return n;
+}
+
+int
+showrefs(Conn *c)
+{
+       int i, ret, nrefs;
+       Hash head, *refs;
+       char **names;
+
+       ret = -1;
+       nrefs = 0;
+       refs = nil;
+       names = nil;
+       if(resolveref(&head, "HEAD") != -1)
+               if(fmtpkt(c, "%H HEAD", head) == -1)
+                       goto error;
+
+       if((nrefs = listrefs(&refs, &names)) == -1)
+               sysfatal("listrefs: %r");
+       for(i = 0; i < nrefs; i++){
+               if(strncmp(names[i], "heads/", strlen("heads/")) != 0)
+                       continue;
+               if(fmtpkt(c, "%H refs/%s\n", refs[i], names[i]) == -1)
+                       goto error;
+       }
+       if(flushpkt(c) == -1)
+               goto error;
+       ret = 0;
+error:
+       for(i = 0; i < nrefs; i++)
+               free(names[i]);
+       free(names);
+       free(refs);
+       return ret;
+}
+
+int
+servnegotiate(Conn *c, Hash **head, int *nhead, Hash **tail, int *ntail)
+{
+       char pkt[Pktmax];
+       int n, acked;
+       Object *o;
+       Hash h;
+
+       if(showrefs(c) == -1)
+               return -1;
+
+       *head = nil;
+       *tail = nil;
+       *nhead = 0;
+       *ntail = 0;
+       while(1){
+               if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
+                       goto error;
+               if(n == 0)
+                       break;
+               if(strncmp(pkt, "want ", 5) != 0){
+                       werrstr(" protocol garble %s", pkt);
+                       goto error;
+               }
+               if(hparse(&h, &pkt[5]) == -1){
+                       werrstr(" garbled want");
+                       goto error;
+               }
+               if((o = readobject(h)) == nil){
+                       werrstr("requested nonexistent object");
+                       goto error;
+               }
+               unref(o);
+               *head = erealloc(*head, (*nhead + 1)*sizeof(Hash));
+               (*head)[*nhead] = h;    
+               *nhead += 1;
+       }
+
+       acked = 0;
+       while(1){
+               if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
+                       goto error;
+               if(strncmp(pkt, "done", 4) == 0)
+                       break;
+               if(n == 0){
+                       if(!acked && fmtpkt(c, "NAK") == -1)
+                                       goto error;
+               }
+               if(strncmp(pkt, "have ", 5) != 0){
+                       werrstr(" protocol garble %s", pkt);
+                       goto error;
+               }
+               if(hparse(&h, &pkt[5]) == -1){
+                       werrstr(" garbled have");
+                       goto error;
+               }
+               if((o = readobject(h)) == nil)
+                       continue;
+               if(!acked){
+                       if(fmtpkt(c, "ACK %H", h) == -1)
+                               goto error;
+                       acked = 1;
+               }
+               unref(o);
+               *tail = erealloc(*tail, (*ntail + 1)*sizeof(Hash));
+               (*tail)[*ntail] = h;    
+               *ntail += 1;
+       }
+       if(!acked && fmtpkt(c, "NAK\n") == -1)
+               goto error;
+       return 0;
+error:
+       fmtpkt(c, "ERR %r\n");
+       free(*head);
+       free(*tail);
+       return -1;
+}
+
+int
+servpack(Conn *c)
+{
+       Hash *head, *tail, h;
+       int nhead, ntail;
+
+       dprint(1, "negotiating pack\n");
+       if(servnegotiate(c, &head, &nhead, &tail, &ntail) == -1)
+               sysfatal("negotiate: %r");
+       dprint(1, "writing pack\n");
+       if(writepack(c->wfd, head, nhead, tail, ntail, &h) == -1)
+               sysfatal("send: %r");
+       return 0;
+}
+
+int
+validref(char *s)
+{
+       if(strncmp(s, "refs/", 5) != 0)
+               return 0;
+       for(; *s != '\0'; s++)
+               if(!isalnum(*s) && strchr("/-_.", *s) == nil)
+                       return 0;
+       return 1;
+}
+
+int
+recvnegotiate(Conn *c, Hash **cur, Hash **upd, char ***ref, int *nupd)
+{
+       char pkt[Pktmax], *sp[4];
+       Hash old, new;
+       int n, i;
+
+       if(showrefs(c) == -1)
+               return -1;
+       *cur = nil;
+       *upd = nil;
+       *ref = nil;
+       *nupd = 0;
+       while(1){
+               if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
+                       goto error;
+               if(n == 0)
+                       break;
+               if(getfields(pkt, sp, nelem(sp), 1, " \t\n\r") != 3){
+                       fmtpkt(c, "ERR  protocol garble %s\n", pkt);
+                       goto error;
+               }
+               if(hparse(&old, sp[0]) == -1){
+                       fmtpkt(c, "ERR bad old hash %s\n", sp[0]);
+                       goto error;
+               }
+               if(hparse(&new, sp[1]) == -1){
+                       fmtpkt(c, "ERR bad new hash %s\n", sp[1]);
+                       goto error;
+               }
+               if(!validref(sp[2])){
+                       fmtpkt(c, "ERR invalid ref %s\n", sp[2]);
+                       goto error;
+               }
+               *cur = erealloc(*cur, (*nupd + 1)*sizeof(Hash));
+               *upd = erealloc(*upd, (*nupd + 1)*sizeof(Hash));
+               *ref = erealloc(*ref, (*nupd + 1)*sizeof(Hash));
+               (*cur)[*nupd] = old;
+               (*upd)[*nupd] = new;
+               (*ref)[*nupd] = estrdup(sp[2]);
+               *nupd += 1;
+       }               
+       return 0;
+error:
+       free(*cur);
+       free(*upd);
+       for(i = 0; i < *nupd; i++)
+               free((*ref)[i]);
+       free(*ref);
+       return -1;
+}
+
+int
+rename(char *pack, char *idx, Hash h)
+{
+       char name[128], path[196];
+       Dir st;
+
+       nulldir(&st);
+       st.name = name;
+       snprint(name, sizeof(name), "%H.pack", h);
+       snprint(path, sizeof(path), ".git/objects/pack/%s", name);
+       if(access(path, AEXIST) == 0)
+               fprint(2, "warning, pack %s already pushed\n", name);
+       else if(dirwstat(pack, &st) == -1)
+               return -1;
+       snprint(name, sizeof(name), "%H.idx", h);
+       snprint(path, sizeof(path), ".git/objects/pack/%s", name);
+       if(access(path, AEXIST) == 0)
+               fprint(2, "warning, pack %s already indexed\n", name);
+       else if(dirwstat(idx, &st) == -1)
+               return -1;
+       return 0;
+}
+
+int
+checkhash(int fd, vlong sz, Hash *hcomp)
+{
+       DigestState *st;
+       Hash hexpect;
+       char buf[Pktmax];
+       vlong n, r;
+       int nr;
+       
+       if(sz < 28){
+               werrstr("undersize packfile");
+               return -1;
+       }
+
+       st = nil;
+       n = 0;
+       if(seek(fd, 0, 0) == -1)
+               sysfatal("packfile seek: %r");
+       while(n != sz - 20){
+               nr = sizeof(buf);
+               if(sz - n - 20 < sizeof(buf))
+                       nr = sz - n - 20;
+               r = readn(fd, buf, nr);
+               if(r != nr){
+                       werrstr("short read");
+                       return -1;
+               }
+               st = sha1((uchar*)buf, nr, nil, st);
+               n += r;
+       }
+       sha1(nil, 0, hcomp->h, st);
+       if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h))
+               sysfatal("truncated packfile");
+       if(!hasheq(hcomp, &hexpect)){
+               werrstr("bad hash: %H != %H", *hcomp, hexpect);
+               return -1;
+       }
+       return 0;
+}
+
+int
+mkdir(char *dir)
+{
+       char buf[ERRMAX];
+       int f;
+
+       if(access(dir, AEXIST) == 0)
+               return 0;
+       if((f = create(dir, OREAD, DMDIR | 0755)) == -1){
+               rerrstr(buf, sizeof(buf));
+               if(strstr(buf, "exist") == nil)
+                       return -1;
+       }
+       close(f);
+       return 0;
+}
+
+int
+updatepack(Conn *c)
+{
+       char buf[Pktmax], packtmp[128], idxtmp[128], ebuf[ERRMAX];
+       int n, pfd, packsz;
+       Hash h;
+
+       /* make sure the needed dirs exist */
+       if(mkdir(".git/objects") == -1)
+               return -1;
+       if(mkdir(".git/objects/pack") == -1)
+               return -1;
+       if(mkdir(".git/refs") == -1)
+               return -1;
+       if(mkdir(".git/refs/heads") == -1)
+               return -1;
+       snprint(packtmp, sizeof(packtmp), ".git/objects/pack/recv-%d.pack.tmp", getpid());
+       snprint(idxtmp, sizeof(idxtmp), ".git/objects/pack/recv-%d.idx.tmp", getpid());
+       if((pfd = create(packtmp, ORDWR, 0644)) == -1)
+               return -1;
+       packsz = 0;
+       while(1){
+               n = read(c->rfd, buf, sizeof(buf));
+               if(n == 0)
+                       break;
+               if(n == -1){
+                       rerrstr(ebuf, sizeof(ebuf));
+                       if(strstr(ebuf, "hungup") == nil)
+                               return -1;
+                       break;
+               }
+               if(write(pfd, buf, n) != n)
+                       return -1;
+               packsz += n;
+       }
+       if(checkhash(pfd, packsz, &h) == -1){
+               dprint(1, "hash mismatch\n");
+               goto error1;
+       }
+       if(indexpack(packtmp, idxtmp, h) == -1){
+               dprint(1, "indexing failed: %r\n");
+               goto error1;
+       }
+       if(rename(packtmp, idxtmp, h) == -1){
+               dprint(1, "rename failed: %r\n");
+               goto error2;
+       }
+       return 0;
+
+error2:        remove(idxtmp);
+error1:        remove(packtmp);
+       return -1;
+}      
+
+int
+lockrepo(void)
+{
+       int fd, i;
+
+       for(i = 0; i < 10; i++) {
+               if((fd = create(".git/_lock", ORCLOSE|ORDWR|OTRUNC|OEXCL, 0644))!= -1)
+                       return fd;
+               sleep(250);
+       }
+       return -1;
+}
+
+int
+updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd)
+{
+       char refpath[512];
+       int i, newidx, hadref, fd, ret, lockfd;
+       vlong newtm;
+       Object *o;
+       Hash h;
+
+       ret = -1;
+       hadref = 0;
+       newidx = -1;
+       /*
+        * Date of Magna Carta.
+        * Wrong because it  was computed using
+        * the proleptic gregorian calendar.
+        */
+       newtm = -23811206400;   
+       if((lockfd = lockrepo()) == -1){
+               werrstr("repo locked\n");
+               return -1;
+       }
+       for(i = 0; i < nupd; i++){
+               if(resolveref(&h, ref[i]) == 0){
+                       hadref = 1;
+                       if(!hasheq(&h, &cur[i])){
+                               werrstr("old ref changed: %s", ref[i]);
+                               goto error;
+                       }
+               }
+               if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){
+                       werrstr("ref path too long: %s", ref[i]);
+                       goto error;
+               }
+               if(hasheq(&upd[i], &Zhash)){
+                       remove(refpath);
+                       continue;
+               }
+               if((o = readobject(upd[i])) == nil){
+                       werrstr("update to nonexistent hash %H", upd[i]);
+                       goto error;
+               }
+               if(o->type != GCommit){
+                       werrstr("not commit: %H", upd[i]);
+                       goto error;
+               }
+               if(o->commit->mtime > newtm){
+                       newtm = o->commit->mtime;
+                       newidx = i;
+               }
+               unref(o);
+               if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){
+                       werrstr("open ref: %r");
+                       goto error;
+               }
+               if(fprint(fd, "%H", upd[i]) == -1){
+                       werrstr("upate ref: %r");
+                       close(fd);
+                       goto error;
+               }
+               close(fd);
+       }
+       /*
+        * Heuristic:
+        * If there are no valid refs, and HEAD is invalid, then
+        * pick the ref with the newest commits as the default
+        * branch.
+        *
+        * Several people have been caught out by pushing to
+        * a repo where HEAD named differently from what got
+        * pushed, and this is going to be more of a footgun
+        * when 'master', 'main', and 'front' are all in active
+        * use. This should make us pick a useful default in
+        * those cases, instead of silently failing.
+        */
+       if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){
+               if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){
+                       werrstr("open HEAD: %r");
+                       goto error;
+               }
+               if(fprint(fd, "ref: %s", ref[0]) == -1){
+                       werrstr("write HEAD ref: %r");
+                       goto error;
+               }
+               close(fd);
+       }
+       ret = 0;
+error:
+       fmtpkt(c, "ERR %r");
+       close(lockfd);
+       return ret;
+}
+
+int
+recvpack(Conn *c)
+{
+       Hash *cur, *upd;
+       char **ref;
+       int nupd;
+
+       if(recvnegotiate(c, &cur, &upd, &ref, &nupd) == -1)
+               sysfatal("negotiate refs: %r");
+       if(nupd != 0 && updatepack(c) == -1)
+               sysfatal("update pack: %r");
+       if(nupd != 0 && updaterefs(c, cur, upd, ref, nupd) == -1)
+               sysfatal("update refs: %r");
+       return 0;
+}
+
+char*
+parsecmd(char *buf, char *cmd, int ncmd)
+{
+       int i;
+       char *p;
+
+       for(p = buf, i = 0; *p && i < ncmd - 1; i++, p++){
+               if(*p == ' ' || *p == '\t'){
+                       cmd[i] = 0;
+                       break;
+               }
+               cmd[i] = *p;
+       }
+       while(*p == ' ' || *p == '\t')
+               p++;
+       return p;
+}
+
+void
+usage(void)
+{
+       fprint(2, "usage: %s [-dw] [-r rel]\n", argv0);
+       exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+       char *repo, cmd[32], buf[512];
+       char *user;
+       Conn c;
+
+       ARGBEGIN{
+       case 'd':
+               chattygit++;
+               break;
+       case 'r':
+               pathpfx = EARGF(usage());
+               if(*pathpfx != '/')
+                       sysfatal("path prefix must begin with '/'");
+               break;
+       case 'n':
+               namespace=EARGF(usage());
+               break;
+       case 'w':
+               allowwrite++;
+               break;
+       default:
+               usage();
+               break;
+       }ARGEND;
+
+       gitinit();
+       user = "none";
+       interactive = 0;
+       if(allowwrite)
+               user = getuser();
+       if(newns(user, namespace) == -1)
+               sysfatal("addns: %r");
+       if(bind(pathpfx, "/", MREPL) == -1)
+               sysfatal("bind: %r");
+       if(rfork(RFNOMNT) == -1)
+               sysfatal("rfork: %r");
+
+       initconn(&c, 0, 1);
+       if(readpkt(&c, buf, sizeof(buf)) == -1)
+               sysfatal("readpkt: %r");
+       repo = parsecmd(buf, cmd, sizeof(cmd));
+       cleanname(repo);
+       if(strncmp(repo, "../", 3) == 0)
+               sysfatal("invalid path %s\n", repo);
+       if(bind(repo, "/", MREPL) == -1){
+               fmtpkt(&c, "ERR no repo %r\n");
+               sysfatal("enter %s: %r", repo);
+       }
+       if(chdir("/") == -1)
+               sysfatal("chdir: %r");
+       if(access(".git", AREAD) == -1)
+               sysfatal("no git repository");
+       if(strcmp(cmd, "git-receive-pack") == 0 && allowwrite)
+               recvpack(&c);
+       else if(strcmp(cmd, "git-upload-pack") == 0)
+               servpack(&c);
+       else
+               sysfatal("unsupported command '%s'", cmd);
+       exits(nil);
+}
diff --git a/sys/src/cmd/git/util.c b/sys/src/cmd/git/util.c
new file mode 100644 (file)
index 0000000..2061751
--- /dev/null
@@ -0,0 +1,321 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+
+#include "git.h"
+
+Reprog *authorpat;
+Hash Zhash;
+
+int chattygit;
+int interactive = 1;
+
+Object*
+emptydir(void)
+{
+       static Object *e;
+
+       if(e != nil)
+               return ref(e);
+       e = emalloc(sizeof(Object));
+       e->hash = Zhash;
+       e->type = GTree;
+       e->tree = emalloc(sizeof(Tinfo));
+       e->tree->ent = nil;
+       e->tree->nent = 0;
+       e->flag |= Cloaded|Cparsed;
+       e->off = -1;
+       ref(e);
+       cache(e);
+       return e;
+}
+
+int
+hasheq(Hash *a, Hash *b)
+{
+       return memcmp(a->h, b->h, sizeof(a->h)) == 0;
+}
+
+static int
+charval(int c, int *err)
+{
+       if(c >= '0' && c <= '9')
+               return c - '0';
+       if(c >= 'a' && c <= 'f')
+               return c - 'a' + 10;
+       if(c >= 'A' && c <= 'F')
+               return c - 'A' + 10;
+       *err = 1;
+       return -1;
+}
+
+void *
+emalloc(ulong n)
+{
+       void *v;
+       
+       v = mallocz(n, 1);
+       if(v == nil)
+               sysfatal("malloc: %r");
+       setmalloctag(v, getcallerpc(&n));
+       return v;
+}
+
+void *
+eamalloc(ulong n, ulong sz)
+{
+       uvlong na;
+       void *v;
+
+       if((na = (uvlong)n*(uvlong)sz) >= (1ULL<<30))
+               sysfatal("alloc: overflow");
+       v = mallocz(na, 1);
+       if(v == nil)
+               sysfatal("malloc: %r");
+       setmalloctag(v, getcallerpc(&n));
+       return v;
+}
+
+void *
+erealloc(void *p, ulong n)
+{
+       void *v;
+       
+       v = realloc(p, n);
+       if(v == nil)
+               sysfatal("realloc: %r");
+       setmalloctag(v, getcallerpc(&p));
+       return v;
+}
+
+void *
+earealloc(void *p, ulong n, ulong sz)
+{
+       uvlong na;
+       void *v;
+
+       if((na = (uvlong)n*(uvlong)sz) >= (1ULL<<30))
+               sysfatal("alloc: overflow");
+       v = realloc(p, na);
+       if(v == nil)
+               sysfatal("realloc: %r");
+       setmalloctag(v, getcallerpc(&p));
+       return v;
+}
+
+char*
+estrdup(char *s)
+{
+       s = strdup(s);
+       if(s == nil)
+               sysfatal("strdup: %r");
+       setmalloctag(s, getcallerpc(&s));
+       return s;
+}
+
+int
+Hfmt(Fmt *fmt)
+{
+       Hash h;
+       int i, n, l;
+       char c0, c1;
+
+       l = 0;
+       h = va_arg(fmt->args, Hash);
+       for(i = 0; i < sizeof h.h; i++){
+               n = (h.h[i] >> 4) & 0xf;
+               c0 = (n >= 10) ? n-10 + 'a' : n + '0';
+               n = h.h[i] & 0xf;
+               c1 = (n >= 10) ? n-10 + 'a' : n + '0';
+               l += fmtprint(fmt, "%c%c", c0, c1);
+       }
+       return l;
+}
+
+int
+Tfmt(Fmt *fmt)
+{
+       int t;
+       int l;
+
+       t = va_arg(fmt->args, int);
+       switch(t){
+       case GNone:     l = fmtprint(fmt, "none");      break;
+       case GCommit:   l = fmtprint(fmt, "commit");    break;
+       case GTree:     l = fmtprint(fmt, "tree");      break;
+       case GBlob:     l = fmtprint(fmt, "blob");      break;
+       case GTag:      l = fmtprint(fmt, "tag");       break;
+       case GOdelta:   l = fmtprint(fmt, "odelta");    break;
+       case GRdelta:   l = fmtprint(fmt, "gdelta");    break;
+       default:        l = fmtprint(fmt, "?%d?", t);   break;
+       }
+       return l;
+}
+
+int
+Ofmt(Fmt *fmt)
+{
+       Object *o;
+       int l;
+
+       o = va_arg(fmt->args, Object *);
+       print("== %H (%T) ==\n", o->hash, o->type);
+       switch(o->type){
+       case GTree:
+               l = fmtprint(fmt, "tree\n");
+               break;
+       case GBlob:
+               l = fmtprint(fmt, "blob %s\n", o->data);
+               break;
+       case GCommit:
+               l = fmtprint(fmt, "commit\n");
+               break;
+       case GTag:
+               l = fmtprint(fmt, "tag\n");
+               break;
+       default:
+               l = fmtprint(fmt, "invalid: %d\n", o->type);
+               break;
+       }
+       return l;
+}
+
+int
+Qfmt(Fmt *fmt)
+{
+       Qid q;
+
+       q = va_arg(fmt->args, Qid);
+       return fmtprint(fmt, "Qid{path=0x%llx(dir:%d,obj:%lld), vers=%ld, type=%d}",
+           q.path, QDIR(&q), (q.path >> 8), q.vers, q.type);
+}
+
+void
+gitinit(void)
+{
+       fmtinstall('H', Hfmt);
+       fmtinstall('T', Tfmt);
+       fmtinstall('O', Ofmt);
+       fmtinstall('Q', Qfmt);
+       inflateinit();
+       deflateinit();
+       authorpat = regcomp("[\t ]*(.*)[\t ]+([0-9]+)[\t ]+([\\-+]?[0-9]+)");
+       osinit(&objcache);
+}
+
+int
+hparse(Hash *h, char *b)
+{
+       int i, err;
+
+       err = 0;
+       for(i = 0; i < sizeof(h->h); i++){
+               err = 0;
+               h->h[i] = 0;
+               h->h[i] |= ((charval(b[2*i], &err) & 0xf) << 4);
+               h->h[i] |= ((charval(b[2*i+1], &err)& 0xf) << 0);
+               if(err){
+                       werrstr("invalid hash");
+                       return -1;
+               }
+       }
+       return 0;
+}
+
+int
+slurpdir(char *p, Dir **d)
+{
+       int r, f;
+
+       if((f = open(p, OREAD)) == -1)
+               return -1;
+       r = dirreadall(f, d);
+       close(f);
+       return r;
+}      
+
+int
+hassuffix(char *base, char *suf)
+{
+       int nb, ns;
+
+       nb = strlen(base);
+       ns = strlen(suf);
+       if(ns <= nb && strcmp(base + (nb - ns), suf) == 0)
+               return 1;
+       return 0;
+}
+
+int
+swapsuffix(char *dst, int dstsz, char *base, char *oldsuf, char *suf)
+{
+       int bl, ol, sl, l;
+
+       bl = strlen(base);
+       ol = strlen(oldsuf);
+       sl = strlen(suf);
+       l = bl + sl - ol;
+       if(l + 1 > dstsz || ol > bl)
+               return -1;
+       memmove(dst, base, bl - ol);
+       memmove(dst + bl - ol, suf, sl);
+       dst[l] = 0;
+       return l;
+}
+
+char *
+strip(char *s)
+{
+       char *e;
+
+       while(isspace(*s))
+               s++;
+       e = s + strlen(s);
+       while(e > s && isspace(*--e))
+               *e = 0;
+       return s;
+}
+
+void
+_dprint(char *fmt, ...)
+{
+       va_list ap;
+
+       va_start(ap, fmt);
+       vfprint(2, fmt, ap);
+       va_end(ap);
+}
+
+/* Finds the directory containing the git repo. */
+int
+findrepo(char *buf, int nbuf)
+{
+       char *p, *suff;
+
+       suff = "/.git/HEAD";
+       if(getwd(buf, nbuf - strlen(suff) - 1) == nil)
+               return -1;
+
+       for(p = buf + strlen(buf); p != nil; p = strrchr(buf, '/')){
+               strcpy(p, suff);
+               if(access(buf, AEXIST) == 0){
+                       p[p == buf] = '\0';
+                       return 0;
+               }
+               *p = '\0';
+       }
+       werrstr("not a git repository");
+       return -1;
+}
+
+int
+showprogress(int x, int pct)
+{
+       if(!interactive)
+               return 0;
+       if(x > pct){
+               pct = x;
+               fprint(2, "\b\b\b\b%3d%%", pct);
+       }
+       return pct;
+}
diff --git a/sys/src/cmd/git/walk.c b/sys/src/cmd/git/walk.c
new file mode 100644 (file)
index 0000000..c50ac28
--- /dev/null
@@ -0,0 +1,333 @@
+#include <u.h>
+#include <libc.h>
+#include "git.h"
+
+#define NCACHE 4096
+#define TDIR ".git/index9/tracked"
+#define RDIR ".git/index9/removed"
+#define HDIR "/mnt/git/HEAD/tree"
+typedef struct Cache   Cache;
+typedef struct Wres    Wres;
+struct Cache {
+       Dir*    cache;
+       int     n;
+       int     max;
+};
+
+struct Wres {
+       char    **path;
+       int     npath;
+       int     pathsz;
+};
+
+enum {
+       Rflg    = 1 << 0,
+       Mflg    = 1 << 1,
+       Aflg    = 1 << 2,
+       Tflg    = 1 << 3,
+};
+
+Cache seencache[NCACHE];
+int quiet;
+int printflg;
+char *rstr = "R ";
+char *tstr = "T ";
+char *mstr = "M ";
+char *astr = "A ";
+
+int
+seen(Dir *dir)
+{
+       Dir *dp;
+       int i;
+       Cache *c;
+
+       c = &seencache[dir->qid.path&(NCACHE-1)];
+       dp = c->cache;
+       for(i=0; i<c->n; i++, dp++)
+               if(dir->qid.path == dp->qid.path &&
+                  dir->type == dp->type &&
+                  dir->dev == dp->dev)
+                       return 1;
+       if(c->n == c->max){
+               if (c->max == 0)
+                       c->max = 8;
+               else
+                       c->max += c->max/2;
+               c->cache = realloc(c->cache, c->max*sizeof(Dir));
+               if(c->cache == nil)
+                       sysfatal("realloc: %r");
+       }
+       c->cache[c->n++] = *dir;
+       return 0;
+}
+
+void
+grow(Wres *r)
+{
+       if(r->npath == r->pathsz){
+               r->pathsz = 2*r->pathsz + 1;
+               r->path = erealloc(r->path, r->pathsz * sizeof(char*));
+       }
+}
+
+int
+readpaths(Wres *r, char *pfx, char *dir)
+{
+       char *f, *sub, *full, *sep;
+       Dir *d;
+       int fd, ret, i, n;
+
+       d = nil;
+       ret = -1;
+       sep = "";
+       if(dir[0] != 0)
+               sep = "/";
+       if((full = smprint("%s/%s", pfx, dir)) == nil)
+               sysfatal("smprint: %r");
+       if((fd = open(full, OREAD)) < 0)
+               goto error;
+       while((n = dirread(fd, &d)) > 0){
+               for(i = 0; i < n; i++){
+                       if(seen(&d[i]))
+                               continue;
+                       if(d[i].qid.type & QTDIR){
+                               if((sub = smprint("%s%s%s", dir, sep, d[i].name)) == nil)
+                                       sysfatal("smprint: %r");
+                               if(readpaths(r, pfx, sub) == -1){
+                                       free(sub);
+                                       goto error;
+                               }
+                               free(sub);
+                       }else{
+                               grow(r);
+                               if((f = smprint("%s%s%s", dir, sep, d[i].name)) == nil)
+                                       sysfatal("smprint: %r");
+                               r->path[r->npath++] = f;
+                       }
+               }
+               free(d);
+       }
+       ret = r->npath;
+error:
+       close(fd);
+       free(full);
+       return ret;
+}
+
+int
+cmp(void *pa, void *pb)
+{
+       return strcmp(*(char **)pa, *(char **)pb);
+}
+
+void
+dedup(Wres *r)
+{
+       int i, o;
+
+       if(r->npath <= 1)
+               return;
+       o = 0;
+       qsort(r->path, r->npath, sizeof(r->path[0]), cmp);
+       for(i = 1; i < r->npath; i++)
+               if(strcmp(r->path[o], r->path[i]) != 0)
+                       r->path[++o] = r->path[i];
+       r->npath = o + 1;
+}
+
+int
+sameqid(Dir *d, char *qf)
+{
+       char indexqid[64], fileqid[64], *p;
+       int fd, n;
+
+       if(!d)
+               return 0;
+       if((fd = open(qf, OREAD)) == -1)
+               return 0;
+       if((n = readn(fd, indexqid, sizeof(indexqid) - 1)) == -1)
+               return 0;
+       indexqid[n] = 0;
+       close(fd);
+       if((p = strpbrk(indexqid, "  \t\n\r")) != nil)
+               *p = 0;
+
+       snprint(fileqid, sizeof(fileqid), "%ullx.%uld.%.2uhhx",
+               d->qid.path, d->qid.vers, d->qid.type);
+
+       if(strcmp(indexqid, fileqid) == 0)
+               return 1;
+       return 0;
+}
+
+void
+writeqid(Dir *d, char *qf)
+{
+       int fd;
+
+       if((fd = create(qf, OWRITE, 0666)) == -1)
+               return;
+       fprint(fd, "%ullx.%uld.%.2uhhx\n",
+               d->qid.path, d->qid.vers, d->qid.type);
+       close(fd);
+}
+
+int
+samedata(char *pa, char *pb)
+{
+       char ba[32*1024], bb[32*1024];
+       int fa, fb, na, nb, same;
+
+       same = 0;
+       fa = open(pa, OREAD);
+       fb = open(pb, OREAD);
+       if(fa == -1 || fb == -1){
+               goto mismatch;
+       }
+       while(1){
+               if((na = readn(fa, ba, sizeof(ba))) == -1)
+                       goto mismatch;
+               if((nb = readn(fb, bb, sizeof(bb))) == -1)
+                       goto mismatch;
+               if(na != nb)
+                       goto mismatch;
+               if(na == 0)
+                       break;
+               if(memcmp(ba, bb, na) != 0)
+                       goto mismatch;
+       }
+       same = 1;
+mismatch:
+       if(fa != -1)
+               close(fa);
+       if(fb != -1)
+               close(fb);
+       return same;
+}
+
+void
+usage(void)
+{
+       fprint(2, "usage: %s [-qbc] [-f filt] [paths...]\n", argv0);
+       exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+       char *rpath, *tpath, *bpath, buf[8], repo[512];
+       char *p, *e;
+       int i, dirty;
+       Wres r;
+       Dir *d;
+
+       ARGBEGIN{
+       case 'q':
+               quiet++;
+               break;
+       case 'c':
+               rstr = "";
+               tstr = "";
+               mstr = "";
+               astr = "";
+               break;
+       case 'f':
+               for(p = EARGF(usage()); *p; p++)
+                       switch(*p){
+                       case 'T':       printflg |= Tflg;       break;
+                       case 'A':       printflg |= Aflg;       break;
+                       case 'M':       printflg |= Mflg;       break;
+                       case 'R':       printflg |= Rflg;       break;
+                       default:        usage();                break;
+               }
+               break;
+       default:
+               usage();
+       }ARGEND
+
+       if(access("/mnt/git/ctl", AEXIST) != 0)
+               sysfatal("no running git/fs");
+       if(findrepo(repo, sizeof(repo)) == -1)
+               sysfatal("find root: %r");
+       if(chdir(repo) == -1)
+               sysfatal("chdir: %r");
+       dirty = 0;
+       memset(&r, 0, sizeof(r));
+       if(access("/mnt/git/ctl", AEXIST) != 0)
+               sysfatal("git/fs does not seem to be running");
+       if(printflg == 0)
+               printflg = Tflg | Aflg | Mflg | Rflg;
+       if(argc == 0){
+               if(access(TDIR, AEXIST) == 0 && readpaths(&r, TDIR, "") == -1)
+                       sysfatal("read tracked: %r");
+               if(access(RDIR, AEXIST) == 0 && readpaths(&r, RDIR, "") == -1)
+                       sysfatal("read removed: %r");
+       }else{
+               for(i = 0; i < argc; i++){
+                       tpath = smprint(TDIR"/%s", argv[i]);
+                       rpath = smprint(RDIR"/%s", argv[i]);
+                       if((d = dirstat(tpath)) == nil && (d = dirstat(rpath)) == nil)
+                               goto nextarg;
+                       if(d->mode & DMDIR){
+                               readpaths(&r, TDIR, argv[i]);
+                               readpaths(&r, RDIR, argv[i]);
+                       }else{
+                               grow(&r);
+                               r.path[r.npath++] = estrdup(argv[i]);
+                       }
+nextarg:
+                       free(tpath);
+                       free(rpath);
+                       free(d);
+               }
+       }
+       dedup(&r);
+
+       for(i = 0; i < r.npath; i++){
+               p = r.path[i];
+               d = dirstat(p);
+               if(d && d->mode & DMDIR)
+                       goto next;
+               rpath = smprint(RDIR"/%s", p);
+               tpath = smprint(TDIR"/%s", p);
+               bpath = smprint(HDIR"/%s", p);
+               /* Fast path: we don't want to force access to the rpath. */
+               if(d && sameqid(d, tpath)) {
+                       if(!quiet && (printflg & Tflg))
+                               print("%s%s\n", tstr, p);
+               }else{
+                       if(d == nil || access(rpath, AEXIST) == 0){
+                               dirty |= Rflg;
+                               if(!quiet && (printflg & Rflg))
+                                       print("%s%s\n", rstr, p);
+                       }else if(access(bpath, AEXIST) == -1) {
+                               dirty |= Aflg;
+                               if(!quiet && (printflg & Aflg))
+                                       print("%s%s\n", astr, p);
+                       }else if(samedata(p, bpath)){
+                               if(!quiet && (printflg & Tflg))
+                                       print("%s%s\n", tstr, p);
+                               writeqid(d, tpath);
+                       }else{
+                               dirty |= Mflg;
+                               if(!quiet && (printflg & Mflg))
+                                       print("%s%s\n", mstr, p);
+                       }
+               }
+               free(rpath);
+               free(tpath);
+               free(bpath);
+next:
+               free(d);
+       }
+       if(!dirty)
+               exits(nil);
+
+       p = buf;
+       e = buf + sizeof(buf);
+       for(i = 0; (1 << i) != Tflg; i++)
+               if(dirty & (1 << i))
+                       p = seprint(p, e, "%c", "DMAT"[i]);
+       exits(buf);
+}