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