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