shithub: git9

Download patch

ref: 45001b2700f817cc4a3609574031437040de393e
parent: 8da1ef146aa867ec8021aa4acae54b030701ee6e
author: Ori Bernstein <[email protected]>
date: Sat Feb 17 23:54:36 EST 2024

sync with 9front

--- a/conf.c
+++ b/conf.c
@@ -59,7 +59,7 @@
 main(int argc, char **argv)
 {
 	char repo[512], *p, *s;
-	int i, j;
+	int i, j, nrel;
 
 	ARGBEGIN{
 	case 'f':	file[nfile++]=EARGF(usage());	break;
@@ -69,7 +69,7 @@
 	}ARGEND;
 
 	if(findroot){
-		if(findrepo(repo, sizeof(repo)) == -1)
+		if(findrepo(repo, sizeof(repo), &nrel) == -1)
 			sysfatal("%r");
 		print("%s\n", repo);
 		exits(nil);
--- a/fs.c
+++ b/fs.c
@@ -71,11 +71,10 @@
 };
 
 #define Eperm	"permission denied"
-#define Eexist	"does not exist"
+#define Eexist	"file does not exist"
 #define E2long	"path too long"
 #define Enodir	"not a directory"
 #define Erepo	"unable to read repo"
-#define Eobject "invalid object"
 #define Egreg	"wat"
 #define Ebadobj	"invalid object"
 
@@ -305,6 +304,9 @@
 		d->qid.path = qpath(c, i, o->id, Qauthor);
 		break;
 	default:
+		free(d->uid);
+		free(d->gid);
+		free(d->muid);
 		return -1;
 	}
 	return 0;
@@ -453,6 +455,9 @@
 			break;
 	}
 	free(path);
+	for(i = 0; o != nil && i < aux->ncrumb; i++)
+		if(crumb(aux, i)->obj == o)
+			return nil;
 	return o;
 }
 
@@ -482,7 +487,7 @@
 			if(!w)
 				return Ebadobj;
 			q->type = (w->type == GTree) ? QTDIR : 0;
-			q->path = qpath(c, i, w->id, qdir);
+			q->path = qpath(p, i, w->id, qdir);
 			c->mode = m;
 			c->mode |= (w->type == GTree) ? DMDIR|0755 : 0644;
 			c->obj = w;
@@ -618,6 +623,8 @@
 			q->path = qpath(o, Qbranch, c->obj->id, Qcommit);
 		else
 			e = Eexist;
+		if(d != nil)
+			c->mode = d->mode & ~0222;
 		free(d);
 		break;
 	case Qobject:
@@ -625,9 +632,9 @@
 			e = objwalk1(q, o->obj, o, c, name, Qobject, aux);
 		}else{
 			if(hparse(&h, name) == -1)
-				return Eobject;
+				return Ebadobj;
 			if((c->obj = readobject(h)) == nil)
-				return Eobject;
+				return Ebadobj;
 			if(c->obj->type == GBlob || c->obj->type == GTag){
 				c->mode = 0644;
 				q->type = 0;
--- a/get.c
+++ b/get.c
@@ -280,7 +280,7 @@
 		if(hasheq(&have[i], &Zhash) || oshas(&hadobj, have[i]))
 			continue;
 		if((o = readobject(have[i])) == nil)
-			sysfatal("missing object we should have: %H", have[i]);
+			sysfatal("missing exected object: %H", have[i]);
 		if(fmtpkt(c, "have %H", o->hash) == -1)
 			sysfatal("write: %r");
 		enqueueparent(&haveq, o);
--- a/git.h
+++ b/git.h
@@ -10,6 +10,7 @@
 typedef struct Delta	Delta;
 typedef struct Cinfo	Cinfo;
 typedef struct Tinfo	Tinfo;
+typedef struct Ginfo	Ginfo;
 typedef struct Object	Object;
 typedef struct Objset	Objset;
 typedef struct Pack	Pack;
@@ -21,6 +22,7 @@
 typedef struct Dblock	Dblock;
 typedef struct Objq	Objq;
 typedef struct Qelt	Qelt;
+typedef struct Idxent	Idxent;
 
 enum {
 	Pathmax		= 512,
@@ -128,6 +130,7 @@
 	union {
 		Cinfo	*commit;
 		Tinfo	*tree;
+		Ginfo	*tag;
 	};
 };
 
@@ -150,6 +153,18 @@
 	vlong	mtime;
 };
 
+struct Ginfo {
+	/* Tag */
+	Hash	object;
+	char	*tagger;
+	char	*type;
+	char	*tag;
+	char	*msg;
+	int	nmsg;
+	vlong	ctime;
+	vlong	mtime;
+};
+
 struct Objset {
 	Object	**obj;
 	int	nobj;
@@ -190,6 +205,13 @@
 	int	len;
 };
 
+struct Idxent {
+	char	*path;
+	Qid	qid;
+	int	mode;
+	int	order;
+	char	state;
+};
 
 #define GETBE16(b)\
 		((((b)[0] & 0xFFul) <<  8) | \
@@ -301,9 +323,10 @@
 int	hassuffix(char *, char *);
 int	swapsuffix(char *, int, char *, char *, char *);
 char	*strip(char *);
-int	findrepo(char *, int);
+int	findrepo(char *, int, int*);
 int	showprogress(int, int);
 u64int	murmurhash2(void*, usize);
+Qid	parseqid(char*);
 
 /* packing */
 void	dtinit(Dtab *, Object*);
--- a/log.c
+++ b/log.c
@@ -207,6 +207,8 @@
 		sysfatal("resolve %s: %r", c);
 	if((o = readobject(h)) == nil)
 		sysfatal("load %H: %r", h);
+	if(o->type != GCommit)
+		sysfatal("%s: not a commit", c);
 	qinit(&objq);
 	osinit(&done);
 	qput(&objq, o, 0);
@@ -239,7 +241,7 @@
 main(int argc, char **argv)
 {
 	char path[1024], repo[1024], *p, *r;
-	int i, nrepo;
+	int i, nrel, nrepo;
 
 	ARGBEGIN{
 	case 'e':
@@ -259,7 +261,7 @@
 		break;
 	}ARGEND;
 
-	if(findrepo(repo, sizeof(repo)) == -1)
+	if(findrepo(repo, sizeof(repo), &nrel) == -1)
 		sysfatal("find root: %r");
 	nrepo = strlen(repo);
 	if(argc != 0){
@@ -291,5 +293,6 @@
 		showquery(queryexpr);
 	else
 		showcommits(commitid);
+	Bterm(out);
 	exits(nil);
 }
--- a/pack.c
+++ b/pack.c
@@ -66,7 +66,7 @@
 Object *lruhead;
 Object *lrutail;
 vlong	ncache;
-vlong	cachemax = 512*MiB;
+vlong	cachemax = 128*MiB;
 Packf	*packf;
 int	npackf;
 int	openpacks;
@@ -160,7 +160,7 @@
 		ref(o);
 		ncache += o->size;
 	}
-	while(ncache > cachemax && lrutail != nil){
+	while(ncache > cachemax && lrutail != lruhead){
 		p = lrutail;
 		lrutail = p->prev;
 		if(lrutail != nil)
@@ -825,6 +825,7 @@
 {
 	char buf[128];
 	Resub m[4];
+	vlong tz;
 	char *p;
 	int n, nm;
 
@@ -845,10 +846,16 @@
 	memcpy(*name, m[1].sp, nm);
 	buf[nm] = 0;
 	
+	nm = m[3].ep - m[3].sp;
+	memcpy(buf, m[3].sp, nm);
+	buf[nm] = 0;
+	tz = atoll(buf);
+
 	nm = m[2].ep - m[2].sp;
 	memcpy(buf, m[2].sp, nm);
 	buf[nm] = 0;
-	*time = atoll(buf);
+	*time = atoll(buf) + 3600*(tz/100) + 60*(tz%100);
+
 	return 0;
 }
 
@@ -901,6 +908,46 @@
 }
 
 static void
+parsetag(Object *o)
+{
+	char *p, buf[128];
+	int np;
+
+	p = o->data;
+	np = o->size;
+	o->tag = emalloc(sizeof(Ginfo));
+	while(1){
+		if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+			break;
+		if(strcmp(buf, "object") == 0){
+			if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+				sysfatal("invalid commit: tree missing");
+			if(hparse(&o->tag->object, buf) == -1)
+				sysfatal("invalid commit: garbled tree");
+		}else if(strcmp(buf, "tagger") == 0){
+			parseauthor(&p, &np, &o->commit->author, &o->tag->mtime);
+		}else if(strcmp(buf, "type") == 0){
+			if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+				sysfatal("bad tag type");
+			if((o->tag->type = strdup(buf)) == nil)
+				sysfatal("strdup: %r");
+		}else if(strcmp(buf, "tag") == 0){
+			if(scanword(&p, &np, buf, sizeof(buf)) == -1)
+				sysfatal("bad tag type");
+			if((o->tag->type = strdup(buf)) == nil)
+				sysfatal("strdup: %r");
+		}
+		nextline(&p, &np);
+	}
+	while (np && isspace(*p)) {
+		p++;
+		np--;
+	}
+	o->commit->msg = p;
+	o->commit->nmsg = np;
+}
+
+static void
 parsetree(Object *o)
 {
 	int m, a, entsz, nent;
@@ -954,12 +1001,6 @@
 	o->tree->ent = ent;
 	o->tree->nent = nent;
 }
-
-static void
-parsetag(Object *)
-{
-}
-
 void
 parseobject(Object *o)
 {
@@ -1210,10 +1251,8 @@
 			if(objectcrc(f, o) == -1)
 				return -1;
 		}
-		if(n == nvalid){
+		if(n == nvalid)
 			sysfatal("fix point reached too early: %d/%d: %r", nvalid, nobj);
-			goto error;
-		}
 		nvalid = n;
 	}
 	if(interactive)
--- a/proto.c
+++ b/proto.c
@@ -58,8 +58,10 @@
 	char *e;
 	int n;
 
-	if(readn(c->rfd, len, 4) != 4)
-		sysfatal("pktline: short read from transport");
+	if(readn(c->rfd, len, 4) != 4){
+		werrstr("pktline: short read from transport");
+		return -1;
+	}
 	len[4] = 0;
 	n = strtol(len, &e, 16);
 	if(n == 0){
--- a/query.c
+++ b/query.c
@@ -152,10 +152,10 @@
 void
 main(int argc, char **argv)
 {
-	int i, j, n;
+	char *query, repo[512];
+	char *p, *e, *objpfx;
+	int i, j, n, nrel;
 	Hash *h;
-	char *p, *e, *s, *objpfx;
-	char query[2048], repo[512];
 
 	ARGBEGIN{
 	case 'd':	chattygit++;	break;
@@ -170,25 +170,26 @@
 
 	if(argc == 0)
 		usage();
-	if(findrepo(repo, sizeof(repo)) == -1)
+	if(findrepo(repo, sizeof(repo), &nrel) == -1)
 		sysfatal("find root: %r");
 	if(chdir(repo) == -1)
 		sysfatal("chdir: %r");
 	if((objpfx = smprint("%s/.git/fs/object/", repo)) == nil)
 		sysfatal("smprint: %r");
-	s = "";
+	for(i = 0, n = 0; i < argc; i++)
+		n += strlen(argv[i]) + 1;
+	query = emalloc(n+1);
 	p = query;
-	e = query + nelem(query);
-	for(i = 0; i < argc; i++){
-		p = seprint(p, e, "%s%s", s, argv[i]);
-		s = " ";
-	}
-	if((n = resolverefs(&h, query)) == -1)
+	e = query + n;
+	for(i = 0; i < argc; i++)
+		p = seprint(p, e, "%s ", argv[i]);
+	n = resolverefs(&h, query);
+	free(query);
+	if(n == -1)
 		sysfatal("resolve: %r");
 	if(changes){
-		if(n != 2)
-			sysfatal("diff: need 2 commits, got %d", n);
-		diffcommits(h[0], h[1]);
+		for(i = 1; i < n; i++)
+			diffcommits(h[0], h[i]);
 	}else{
 		p = (fullpath ? objpfx : "");
 		for(j = 0; j < n; j++)
--- a/ref.c
+++ b/ref.c
@@ -125,8 +125,10 @@
 	nskip = 0;
 
 	for(i = 0; i < nhead; i++){
+		if(hasheq(&head[i], &Zhash))
+			continue;
 		if((o = readobject(head[i])) == nil){
-			fprint(2, "warning: %H does not point at commit\n", o->hash);
+			fprint(2, "warning: %H does not point at commit\n", head[i]);
 			werrstr("read head %H: %r", head[i]);
 			return -1;
 		}
@@ -140,6 +142,8 @@
 		unref(o);
 	}		
 	for(i = 0; i < ntail; i++){
+		if(hasheq(&tail[i], &Zhash))
+			continue;
 		if((o = readobject(tail[i])) == nil){
 			werrstr("read tail %H: %r", tail[i]);
 			return -1;
@@ -184,6 +188,10 @@
 			break;
 		}
 		o = readobject(e.o->hash);
+		if(o->type != GCommit){
+			werrstr("not a commit: %H", o->hash);
+			goto error;
+		}
 		for(i = 0; i < o->commit->nparent; i++){
 			if((c = readobject(e.o->commit->parent[i])) == nil)
 				goto error;
@@ -301,6 +309,10 @@
 	Object *o, *p;
 
 	o = pop(ev);
+	if(o->type != GCommit){
+		werrstr("not a commit: %H", o->hash);
+		return -1;
+	}
 	/* Special case: first commit has no parent. */
 	if(o->commit->nparent == 0)
 		p = emptydir();
--- a/save.c
+++ b/save.c
@@ -10,6 +10,7 @@
 	char *dat;
 	int ndat;
 };
+
 enum {
 	Maxparents = 16,
 };
@@ -21,7 +22,9 @@
 char	*commitmsg;
 Hash	parents[Maxparents];
 int	nparents;
-
+Idxent	*idx;
+int	idxsz;
+int	nidx;
 int
 gitmode(Dirent *e)
 {
@@ -31,7 +34,7 @@
 		return 0160000;
 	else if(e->mode & DMDIR)
 		return 0040000;
-	else if(e->mode & 0111)
+	else if(e->mode & 0100)
 		return 0100755;
 	else
 		return 0100644;
@@ -38,6 +41,20 @@
 }
 
 int
+idxcmp(void *pa, void *pb)
+{
+	Idxent *a, *b;
+	int c;
+
+	a = (Idxent*)pa;
+	b = (Idxent*)pb;
+	if((c = strcmp(a->path, b->path)) != 0)
+		return c;
+	assert(a->order != b->order);
+	return a-> order < b->order ? -1 : 1;
+}
+
+int
 entcmp(void *pa, void *pb)
 {
 	char abuf[256], bbuf[256], *ae, *be;
@@ -184,27 +201,21 @@
 int
 tracked(char *path)
 {
-	char ipath[256];
-	Dir *d;
+	int r, lo, hi, mid;
 
-	/* 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;
+	lo = 0;
+	hi = nidx-1;
+	while(lo <= hi){
+		mid = (hi + lo) / 2;
+		r = strcmp(path, idx[mid].path);
+		if(r < 0)
+			hi = mid-1;
+		else if(r > 0)
+			lo = mid+1;
+		else
+			return idx[mid].state != 'R';
 	}
-
-	/* 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;
+	return 0; 
 }
 
 int
@@ -354,10 +365,11 @@
 void
 main(int argc, char **argv)
 {
+	char *ln, *dstr, *parts[4], cwd[1024];
+	int i, r, line, ncwd;
 	Hash th, ch;
-	char *dstr, cwd[1024];
-	int i, r, ncwd;
 	vlong date;
+	Biobuf *f;
 	Object *t;
 
 	gitinit();
@@ -425,6 +437,33 @@
 	}
 
 	t = findroot();
+	nidx = 0;
+	idxsz = 32;
+	idx = emalloc(idxsz*sizeof(Idxent));
+	if((f = Bopen(".git/INDEX9", OREAD)) == nil)
+		sysfatal("open index: %r");
+	line = 0;
+	while((ln = Brdstr(f, '\n', 1)) != nil){
+		line++;
+		if(ln[0] == 0 || ln[0] == '\n')
+			continue;
+		if(getfields(ln, parts, nelem(parts), 0, " \t") != nelem(parts))
+			sysfatal(".git/INDEX9:%d: corrupt index", line);
+		if(nidx == idxsz){
+			idxsz += idxsz/2;
+			idx = realloc(idx, idxsz*sizeof(Idxent));
+		}
+		cleanname(parts[3]);
+		idx[nidx].state = *parts[0];
+		idx[nidx].qid = parseqid(parts[1]);
+		idx[nidx].mode = strtol(parts[2], nil, 8);
+		idx[nidx].path = strdup(parts[3]);
+		idx[nidx].order = nidx;
+		nidx++;
+		free(ln);
+	}
+	Bterm(f);
+	qsort(idx, nidx, sizeof(Idxent), idxcmp);
 	r = treeify(t, argv, argv + argc, 0, &th);
 	if(r == -1)
 		sysfatal("could not commit: %r\n");
--- a/send.c
+++ b/send.c
@@ -75,9 +75,11 @@
 			sysfatal("smprint: %r");
 		if((idx = findref(ref, nu, r)) == -1)
 			idx = nu++;
+		else
+			free(ref[idx]);
 		assert(idx < nremoved + nbranch);
 		memcpy(&tail[idx], &Zhash, sizeof(Hash));
-		free(r);
+		ref[idx] = r;
 	}
 	dprint(1, "nu: %d\n", nu);
 	for(i = 0; i < nu; i++)
@@ -184,7 +186,10 @@
 		p = nil;
 		if(a != nil && b != nil)
 			p = ancestor(a, b);
-		if(!force && !hasheq(&m->theirs, &Zhash) && (a == nil || p != a)){
+		if(!force
+		&& !hasheq(&m->theirs, &Zhash)
+		&& !hasheq(&m->ours, &Zhash)
+		&& (a == nil || p != a)){
 			fprint(2, "remote has diverged\n");
 			werrstr("remote diverged");
 			flushpkt(c);
--- a/serve.c
+++ b/serve.c
@@ -7,28 +7,12 @@
 
 char	*pathpfx = nil;
 int	allowwrite;
-int	report;
 
 int
-parsecaps(char *caps)
-{
-	char *p, *n;
-
-	for(p = caps; p != nil; p = n){
-		if((n = strchr(p, ' ')) != nil)
-			*n++ = 0;
-		if(strcmp(p, "report-status") == 0)
-			report = 1;
-	}
-	return 0;
-}
-
-int
 showrefs(Conn *c)
 {
 	int i, ret, nrefs;
 	Hash head, *refs;
-	char *p, *e, pkt[Pktmax];
 	char **names;
 
 	ret = -1;
@@ -44,14 +28,7 @@
 	for(i = 0; i < nrefs; i++){
 		if(strncmp(names[i], "heads/", strlen("heads/")) != 0)
 			continue;
-		p = pkt;
-		e = pkt+sizeof(pkt);
-		p = seprint(p, e, "%H refs/%s\n", refs[i], names[i]);
-		if(i == 0){
-			*p++ = 0;
-			p = seprint(p, e, "report-status");
-		}
-		if(writepkt(c, pkt, p-pkt) == -1)
+		if(fmtpkt(c, "%H refs/%s\n", refs[i], names[i]) == -1)
 			goto error;
 	}
 	if(flushpkt(c) == -1)
@@ -68,8 +45,8 @@
 int
 servnegotiate(Conn *c, Hash **head, int *nhead, Hash **tail, int *ntail)
 {
-	char *sp[3], pkt[Pktmax];
-	int n, nsp, acked;
+	char pkt[Pktmax];
+	int n, acked;
 	Object *o;
 	Hash h;
 
@@ -85,22 +62,14 @@
 			goto error;
 		if(n == 0)
 			break;
-		if((nsp = getfields(pkt, sp, nelem(sp), 1, " \t")) < 2){
-			werrstr("protocol garble %s", pkt);
-			goto error;
-		}
-		if(strcmp(sp[0], "want") != 0){
+		if(strncmp(pkt, "want ", 5) != 0){
 			werrstr(" protocol garble %s", pkt);
 			goto error;
 		}
-		if(hparse(&h, sp[1]) == -1){
+		if(hparse(&h, &pkt[5]) == -1){
 			werrstr(" garbled want");
 			goto error;
 		}
-		if(nsp > 2 && parsecaps(sp[2]) == -1){
-			werrstr("garbled caps %s", sp[2]);
-			goto error;
-		}
 		if((o = readobject(h)) == nil){
 			werrstr("requested nonexistent object");
 			goto error;
@@ -182,7 +151,7 @@
 {
 	char pkt[Pktmax], *sp[4];
 	Hash old, new;
-	int l, n, i;
+	int n, i;
 
 	if(showrefs(c) == -1)
 		return -1;
@@ -195,11 +164,6 @@
 			goto error;
 		if(n == 0)
 			break;
-		l = strlen(pkt);
-		if(n > l+1 && parsecaps(pkt+l+1) == -1){
-			fmtpkt(c, "ERR  protocol garble %s\n", pkt);
-			goto error;
-		}
 		if(getfields(pkt, sp, nelem(sp), 1, " \t\n\r") != 3){
 			fmtpkt(c, "ERR  protocol garble %s\n", pkt);
 			goto error;
@@ -350,26 +314,21 @@
 		packsz += n;
 	}
 	if(checkhash(pfd, packsz, &h) == -1){
-		werrstr("hash mismatch\n");
+		dprint(1, "hash mismatch\n");
 		goto error1;
 	}
 	if(indexpack(packtmp, idxtmp, h) == -1){
-		werrstr("indexing failed: %r\n");
+		dprint(1, "indexing failed: %r\n");
 		goto error1;
 	}
 	if(rename(packtmp, idxtmp, h) == -1){
-		werrstr("rename failed: %r\n");
+		dprint(1, "rename failed: %r\n");
 		goto error2;
 	}
-	if(report)
-		fmtpkt(c, "unpack ok");
 	return 0;
 
 error2:	remove(idxtmp);
 error1:	remove(packtmp);
-	dprint(1, "update pack: %r");
-	if(report)
-		fmtpkt(c, "unpack %r");
 	return -1;
 }	
 
@@ -389,13 +348,12 @@
 int
 updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd)
 {
-	char refpath[512];
-	int i, j, newidx, hadref, fd, ret, lockfd;
+	char refpath[512], buf[128];
+	int i, newidx, hadref, fd, ret, lockfd;
 	vlong newtm;
 	Object *o;
 	Hash h;
 
-	i = 0;
 	ret = -1;
 	hadref = 0;
 	newidx = -1;
@@ -406,20 +364,20 @@
 	 */
 	newtm = -23811206400;	
 	if((lockfd = lockrepo()) == -1){
-		werrstr("repo locked\n");
-		goto out;
+		snprint(buf, sizeof(buf), "repo locked\n");
+		return -1;
 	}
 	for(i = 0; i < nupd; i++){
 		if(resolveref(&h, ref[i]) == 0){
 			hadref = 1;
 			if(!hasheq(&h, &cur[i])){
-				werrstr("old ref changed: %s", ref[i]);
-				goto out;
+				snprint(buf, sizeof(buf), "old ref changed: %s", ref[i]);
+				goto error;
 			}
 		}
 		if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){
-			werrstr("ref path too long: %s", ref[i]);
-			goto out;
+			snprint(buf, sizeof(buf), "ref path too long: %s", ref[i]);
+			goto error;
 		}
 		if(hasheq(&upd[i], &Zhash)){
 			remove(refpath);
@@ -426,12 +384,12 @@
 			continue;
 		}
 		if((o = readobject(upd[i])) == nil){
-			werrstr("update to nonexistent hash %H", upd[i]);
-			goto out;
+			snprint(buf, sizeof(buf), "update to nonexistent hash %H", upd[i]);
+			goto error;
 		}
 		if(o->type != GCommit){
-			werrstr("not commit: %H", upd[i]);
-			goto out;
+			snprint(buf, sizeof(buf), "not commit: %H", upd[i]);
+			goto error;
 		}
 		if(o->commit->mtime > newtm){
 			newtm = o->commit->mtime;
@@ -439,13 +397,13 @@
 		}
 		unref(o);
 		if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){
-			werrstr("open ref: %r");
-			goto out;
+			snprint(buf, sizeof(buf), "open ref: %r");
+			goto error;
 		}
 		if(fprint(fd, "%H", upd[i]) == -1){
-			werrstr("upate ref: %r");
+			snprint(buf, sizeof(buf), "upate ref: %r");
 			close(fd);
-			goto out;
+			goto error;
 		}
 		close(fd);
 	}
@@ -462,30 +420,22 @@
 	 * use. This should make us pick a useful default in
 	 * those cases, instead of silently failing.
 	 */
-	i = 0;
 	if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){
 		if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){
-			werrstr("open HEAD: %r");
-			goto out;
+			snprint(buf, sizeof(buf), "open HEAD: %r");
+			goto error;
 		}
-		if(fprint(fd, "ref: %s", ref[i]) == -1){
-			werrstr("write HEAD ref: %r");
-			goto out;
+		if(fprint(fd, "ref: %s", ref[0]) == -1){
+			snprint(buf, sizeof(buf), "write HEAD ref: %r");
+			goto error;
 		}
 		close(fd);
 	}
 	ret = 0;
-out:
-	if(report){
-		for(j = 0; j < nupd; j++){
-			if(i != j || ret == 0)
-				fmtpkt(c, "ok %s", ref[i]);
-			else
-				fmtpkt(c, "ng %s %r\n", ref[i]);
-		}
-	}
-	if(lockfd != -1)
-		close(lockfd);
+error:
+	fmtpkt(c, "ERR %s", buf);
+	close(lockfd);
+	werrstr(buf);
 	return ret;
 }
 
--- a/util.c
+++ b/util.c
@@ -192,8 +192,10 @@
 	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);
+	if(q.path == ~0ULL && q.vers == ~0UL && q.type == 0xff)
+		return fmtprint(fmt, "NOQID");
+	else
+		return fmtprint(fmt, "%llux.%lud.%hhx", q.path, q.vers, q.type);
 }
 
 void
@@ -205,7 +207,7 @@
 	fmtinstall('Q', Qfmt);
 	inflateinit();
 	deflateinit();
-	authorpat = regcomp("[\t ]*(.*)[\t ]+([0-9]+)[\t ]+([\\-+]?[0-9]+)");
+	authorpat = regcomp("[\t ]*(.*)[\t ]+([0-9]+)[\t ]*([\\-+]?[0-9]+)?");
 	osinit(&objcache);
 }
 
@@ -294,7 +296,7 @@
 
 /* Finds the directory containing the git repo. */
 int
-findrepo(char *buf, int nbuf)
+findrepo(char *buf, int nbuf, int *nrel)
 {
 	char *p, *suff;
 
@@ -302,6 +304,7 @@
 	if(getwd(buf, nbuf - strlen(suff) - 1) == nil)
 		return -1;
 
+	*nrel = 0;
 	for(p = buf + strlen(buf); p != nil; p = strrchr(buf, '/')){
 		strcpy(p, suff);
 		if(access(buf, AEXIST) == 0){
@@ -308,6 +311,7 @@
 			p[p == buf] = '\0';
 			return 0;
 		}
+		*nrel += 1;
 		*p = '\0';
 	}
 	werrstr("not a git repository");
@@ -347,6 +351,7 @@
 	Qelt t;
 	int i;
 
+	assert(o->type == GCommit);
 	if(q->nheap == q->heapsz){
 		q->heapsz *= 2;
 		q->heap = earealloc(q->heap, q->heapsz, sizeof(Qelt));
@@ -441,4 +446,25 @@
 	h ^= h >> 15;
 
 	return h;
+}
+
+Qid
+parseqid(char *s)
+{
+	char *e;
+	Qid q;
+
+	if(strcmp(s, "NOQID") == 0)
+		return (Qid){-1, -1, -1};		
+	e = s;
+	q.path = strtoull(e, &e, 16);
+	if(*e != '.')
+		sysfatal("corrupt qid: %s (%s)\n", s, e);
+	q.vers = strtoul(e+1, &e, 10);
+	if(*e != '.')
+		sysfatal("corrupt qid: %s (%s)\n", s, e);
+	q.type = strtoul(e+1, &e, 16);
+	if(*e != '\0')
+		sysfatal("corrupt qid: %s (%x)\n", s, *e);
+	return q;
 }
--- a/walk.c
+++ b/walk.c
@@ -2,52 +2,72 @@
 #include <libc.h>
 #include "git.h"
 
+typedef struct Seen	Seen;
+typedef struct Idxed	Idxed;
+typedef struct Idxent	Idxent;
+
 #define NCACHE 4096
-#define TDIR ".git/index9/tracked"
-#define RDIR ".git/index9/removed"
-#define HDIR ".git/fs/HEAD/tree"
-typedef struct Cache	Cache;
-typedef struct Wres	Wres;
-struct Cache {
+
+enum {
+	Rflg	= 1 << 0,
+	Mflg	= 1 << 1,
+	Aflg	= 1 << 2,
+	Uflg	= 1 << 3,
+	/* everything after this is not an error */
+	Tflg	= 1 << 4,
+};
+
+struct Seen {
 	Dir*	cache;
 	int	n;
 	int	max;
 };
 
-struct Wres {
-	char	**path;
-	int	npath;
-	int	pathsz;
+struct Idxed {
+	char**	cache;
+	int	n;
+	int	max;
 };
 
-enum {
-	Rflg	= 1 << 0,
-	Mflg	= 1 << 1,
-	Aflg	= 1 << 2,
-	Tflg	= 1 << 3,
-};
+Seen	seentab[NCACHE];
+Idxed	idxtab[NCACHE];
+char	repopath[1024];
+char	wdirpath[1024];
+char	*rstr	= "R ";
+char	*mstr	= "M ";
+char	*astr	= "A ";
+char	*ustr	= "U ";
+char	*tstr 	= "T ";
+char	*bdir = ".git/fs/HEAD/tree";
+int	useidx	= 1;
+int	nrel;
+int	quiet;
+int	dirty;
+int	printflg;
 
-Cache seencache[NCACHE];
-int quiet;
-int printflg;
-char *rstr = "R ";
-char *tstr = "T ";
-char *mstr = "M ";
-char *astr = "A ";
+Idxent	*idx;
+int	idxsz;
+int	nidx;
+int	staleidx;
+Idxent	*wdir;
+int	wdirsz;
+int	nwdir;
 
+int	loadwdir(char*);
+
 int
 seen(Dir *dir)
 {
+	Seen *c;
 	Dir *dp;
 	int i;
-	Cache *c;
 
-	c = &seencache[dir->qid.path&(NCACHE-1)];
+	c = &seentab[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)
+		if(dir->qid.path == dp->qid.path
+		&& dir->qid.type == dp->qid.type
+		&& dir->dev == dp->dev)
 			return 1;
 	if(c->n == c->max){
 		if (c->max == 0)
@@ -62,129 +82,115 @@
 	return 0;
 }
 
-void
-grow(Wres *r)
-{
-	if(r->npath == r->pathsz){
-		r->pathsz = 2*r->pathsz + 1;
-		r->path = erealloc(r->path, r->pathsz * sizeof(char*));
-	}
-}
-
 int
-readpaths(Wres *r, char *pfx, char *dir)
+checkedin(Idxent *e, int change)
 {
-	char *f, *sub, *full, *sep;
-	Dir *d;
-	int fd, ret, i, n;
+	char *p;
+	int r;
 
-	d = nil;
-	ret = -1;
-	sep = "";
-	if(dir[0] != 0)
-		sep = "/";
-	if((full = smprint("%s/%s", pfx, dir)) == nil)
-		sysfatal("smprint: %r");
-	if((fd = open(full, OREAD)) < 0)
-		goto error;
-	while((n = dirread(fd, &d)) > 0){
-		for(i = 0; i < n; i++){
-			if(seen(&d[i]))
-				continue;
-			if(d[i].qid.type & QTDIR){
-				if((sub = smprint("%s%s%s", dir, sep, d[i].name)) == nil)
-					sysfatal("smprint: %r");
-				if(readpaths(r, pfx, sub) == -1){
-					free(sub);
-					goto error;
-				}
-				free(sub);
-			}else{
-				grow(r);
-				if((f = smprint("%s%s%s", dir, sep, d[i].name)) == nil)
-					sysfatal("smprint: %r");
-				r->path[r->npath++] = f;
-			}
-		}
-		free(d);
+	p = smprint("%s/%s", bdir, e->path);
+	r = access(p, AEXIST);
+	if(r == 0 && change){
+		if(e->state != 'R')
+			e->state = 'T';
+		staleidx = 1;
 	}
-	ret = r->npath;
-error:
-	close(fd);
-	free(full);
-	return ret;
+	free(p);
+	return r == 0;
 }
 
 int
-cmp(void *pa, void *pb)
+indexed(char *path, int isdir)
 {
-	return strcmp(*(char **)pa, *(char **)pb);
-}
+	int lo, hi, mid, n, r;
+	char *s;
 
-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;
+	if(!useidx){
+		s = smprint("%s/%s", bdir, path);
+		r = access(s, AEXIST);
+		free(s);
+		return r == 0;
+	}
+	s = path;
+	if(isdir)
+		s = smprint("%s/", path);
+	r = -1;
+	lo = 0;
+	hi = nidx-1;
+	n = strlen(s);
+	while(lo <= hi){
+		mid = (hi + lo) / 2;
+		if(isdir)
+			r = strncmp(s, idx[mid].path, n);
+		else
+			r = strcmp(s, idx[mid].path);
+		if(r < 0)
+			hi = mid-1;
+		else if(r > 0)
+			lo = mid+1;
+		else
+			break;
+	}
+	if(isdir)
+		free(s);
+	return r == 0;
 }
 
 int
-sameqid(Dir *d, char *qf)
+idxcmp(void *pa, void *pb)
 {
-	char indexqid[64], fileqid[64], *p;
-	int fd, n;
+	Idxent *a, *b;
+	int c;
 
-	if(!d)
-		return 0;
-	if((fd = open(qf, OREAD)) == -1)
-		return 0;
-	if((n = readn(fd, indexqid, sizeof(indexqid) - 1)) == -1)
-		return 0;
-	indexqid[n] = 0;
-	close(fd);
-	if((p = strpbrk(indexqid, "  \t\n\r")) != nil)
-		*p = 0;
-
-	snprint(fileqid, sizeof(fileqid), "%ullx.%uld.%.2uhhx",
-		d->qid.path, d->qid.vers, d->qid.type);
-
-	if(strcmp(indexqid, fileqid) == 0)
-		return 1;
-	return 0;
+	a = (Idxent*)pa;
+	b = (Idxent*)pb;
+	if((c = strcmp(a->path, b->path)) != 0)
+		return c;
+	/* order is unique */
+	return a-> order < b->order ? -1 : 1;
 }
 
-void
-writeqid(Dir *d, char *qf)
-{
-	int fd;
-
-	if((fd = create(qf, OWRITE, 0666)) == -1)
-		return;
-	fprint(fd, "%ullx.%uld.%.2uhhx\n",
-		d->qid.path, d->qid.vers, d->qid.type);
-	close(fd);
-}
-
+/*
+ * compares whether the indexed entry 'a'
+ * has the same contents and mode as
+ * the entry on disk 'b'; if the indexed
+ * entry is nil, does a deep comparison
+ * of the checked out file and the file
+ * checked in.
+ */
 int
-samedata(char *pa, char *pb)
+samedata(Idxent *a, Idxent *b)
 {
-	char ba[32*1024], bb[32*1024];
+	char *gitpath, ba[IOUNIT], bb[IOUNIT];
 	int fa, fb, na, nb, same;
+	Dir *da, *db;
 
+	if(a != nil){
+		if(a->qid.path == b->qid.path
+		&& a->qid.vers == b->qid.vers
+		&& a->qid.type == b->qid.type
+		&& a->mode == b->mode
+		&& a->mode != 0)
+			return 1;
+	}
+
 	same = 0;
-	fa = open(pa, OREAD);
-	fb = open(pb, OREAD);
-	if(fa == -1 || fb == -1){
+	da = nil;
+	db = nil;
+	if((gitpath = smprint("%s/%s", bdir, b->path)) == nil)
+		sysfatal("smprint: %r");
+	fa = open(gitpath, OREAD);
+	fb = open(b->path, OREAD);
+	if(fa == -1 || fb == -1)
 		goto mismatch;
-	}
+	da = dirfstat(fa);
+	db = dirfstat(fb);
+	if(da == nil || db == nil)
+		goto mismatch;
+	if((da->mode&0100) != (db->mode&0100))
+		goto mismatch;
+	if(da->length != db->length)
+		goto mismatch;
 	while(1){
 		if((na = readn(fa, ba, sizeof(ba))) == -1)
 			goto mismatch;
@@ -197,8 +203,16 @@
 		if(memcmp(ba, bb, na) != 0)
 			goto mismatch;
 	}
+	if(a != nil){
+		a->qid = db->qid;
+		a->mode = db->mode;
+		staleidx = 1;
+	}
 	same = 1;
+
 mismatch:
+	free(da);
+	free(db);
 	if(fa != -1)
 		close(fa);
 	if(fb != -1)
@@ -206,10 +220,139 @@
 	return same;
 }
 
+int
+loadent(char *dir, Dir *d, int fullpath)
+{
+	char *path;
+	int ret, isdir;
+	Idxent *e;
+
+	if(fullpath)
+		path = strdup(dir);
+	else
+		path = smprint("%s/%s", dir, d->name);
+	if(path == nil)
+		sysfatal("smprint: %r");
+
+	cleanname(path);
+	if(strncmp(path, ".git/", 5) == 0){
+		free(path);
+		return 0;
+	}
+	ret = 0;
+	isdir = d->qid.type & QTDIR;
+	if((printflg & Uflg) == 0 && !indexed(path, isdir)){
+		free(path);
+		return 0;
+	}
+	if(isdir){
+		ret = loadwdir(path);
+		free(path);
+	}else{
+		if(nwdir == wdirsz){
+			wdirsz += wdirsz/2;
+			wdir = erealloc(wdir, wdirsz*sizeof(Idxent));
+		}
+		e = wdir + nwdir;
+		e->path = path;
+		e->qid = d->qid;
+		e->mode = d->mode;
+		e->order = nwdir;
+		e->state = 'T';
+		nwdir++;
+	}
+	return ret;
+}
+
+int
+loadwdir(char *path)
+{
+	int fd, ret, i, n;
+	Dir *d, *e;
+
+	d = nil;
+	e = nil;
+	ret = -1;
+	cleanname(path);
+	if(strncmp(path, ".git/", 5) == 0)
+		return 0;
+	if((fd = open(path, OREAD)) < 0)
+		goto error;
+	if((e = dirfstat(fd)) == nil)
+		sysfatal("fstat: %r");
+	if(e->qid.type & QTDIR)
+		while((n = dirread(fd, &d)) > 0){
+			for(i = 0; i < n; i++)
+				if(loadent(path, &d[i], 0) == -1)
+					goto error;
+			free(d);
+		}
+	else{
+		if(loadent(path, e, 1) == -1)
+			goto error;
+	}
+	ret = 0;
+error:
+	free(e);
+	if(fd != -1)
+		close(fd);
+	return ret;
+}
+
+int
+pfxmatch(char *p, char **pfx, int *pfxlen, int npfx)
+{
+	int i;
+
+	if(p == nil)
+		return 0;
+	if(npfx == 0)
+		return 1;
+	for(i = 0; i < npfx; i++){
+		if(strncmp(p, pfx[i], pfxlen[i]) != 0)
+			continue;
+		if(p[pfxlen[i]] == '/' || p[pfxlen[i]] == 0)
+			return 1;
+		if(strcmp(pfx[i], ".") == 0 || *pfx[i] == 0)
+			return 1;
+	}
+	return 0;
+}
+
+
+char*
+reporel(char *s)
+{
+	char *p;
+	int n;
+
+	if(*s == '/')
+		s = strdup(s);
+	else
+		s = smprint("%s/%s", wdirpath, s);
+	p = cleanname(s);
+	n = strlen(repopath);
+	if(strncmp(s, repopath, n) != 0)
+		sysfatal("path outside repo: %s", s);
+	p += n;
+	if(*p == '/')
+		p++;
+	memmove(s, p, strlen(p)+1);
+	return s;
+}
+
 void
+show(Biobuf *o, int flg, char *str, char *path)
+{
+	dirty |= flg;
+	if(!quiet && (printflg & flg))
+		Bprint(o, "%s%s\n", str, path);
+}
+
+void
 usage(void)
 {
-	fprint(2, "usage: %s [-qbc] [-f filt] [paths...]\n", argv0);
+	fprint(2, "usage: %s [-qbc] [-f filt] [-b base] [paths...]\n", argv0);
 	exits("usage");
 }
 
@@ -216,12 +359,22 @@
 void
 main(int argc, char **argv)
 {
-	char *rpath, *tpath, *bpath, buf[8], repo[512];
-	char *p, *e;
-	int i, dirty;
-	Wres r;
-	Dir *d;
+	char *p, *e, *ln, *base, **argrel, *parts[4], xbuf[8];
+	int i, j, c, line, wfd, *argn;
+	Biobuf *f, *o, *w;
+	Hash h, hd;
+	Dir rn;
 
+	gitinit();
+	if(access(".git/fs/ctl", AEXIST) != 0)
+		sysfatal("no running git/fs");
+	if(getwd(wdirpath, sizeof(wdirpath)) == nil)
+		sysfatal("getwd: %r");
+	if(findrepo(repopath, sizeof(repopath), &nrel) == -1)
+		sysfatal("find root: %r");
+	if(chdir(repopath) == -1)
+		sysfatal("chdir: %r");
+
 	ARGBEGIN{
 	case 'q':
 		quiet++;
@@ -231,6 +384,7 @@
 		tstr = "";
 		mstr = "";
 		astr = "";
+		ustr = "";
 		break;
 	case 'f':
 		for(p = EARGF(usage()); *p; p++)
@@ -239,95 +393,184 @@
 			case 'A':	printflg |= Aflg;	break;
 			case 'M':	printflg |= Mflg;	break;
 			case 'R':	printflg |= Rflg;	break;
+			case 'U':	printflg |= Uflg;	break;
 			default:	usage();		break;
 		}
 		break;
+	case 'b':
+		useidx = 0;
+		base = EARGF(usage());
+		if(resolveref(&h, base) == -1)
+			sysfatal("no such ref '%s'", base);
+		/* optimization: we're a lot faster when using the index */
+		if(resolveref(&hd, "HEAD") == 0 && hasheq(&h, &hd))
+			useidx = 1;
+		bdir = smprint(".git/fs/object/%H/tree", h);
+		break;
 	default:
 		usage();
-	}ARGEND
+	}ARGEND;
 
-	if(findrepo(repo, sizeof(repo)) == -1)
-		sysfatal("find root: %r");
-	if(chdir(repo) == -1)
-		sysfatal("chdir: %r");
-	if(access(".git/fs/ctl", AEXIST) != 0)
-		sysfatal("no running git/fs");
-	dirty = 0;
-	memset(&r, 0, sizeof(r));
 	if(printflg == 0)
 		printflg = Tflg | Aflg | Mflg | Rflg;
-	if(argc == 0){
-		if(access(TDIR, AEXIST) == 0 && readpaths(&r, TDIR, "") == -1)
-			sysfatal("read tracked: %r");
-		if(access(RDIR, AEXIST) == 0 && readpaths(&r, RDIR, "") == -1)
-			sysfatal("read removed: %r");
-	}else{
-		for(i = 0; i < argc; i++){
-			tpath = smprint(TDIR"/%s", argv[i]);
-			rpath = smprint(RDIR"/%s", argv[i]);
-			if((d = dirstat(tpath)) == nil && (d = dirstat(rpath)) == nil)
-				goto nextarg;
-			if(d->mode & DMDIR){
-				readpaths(&r, TDIR, argv[i]);
-				readpaths(&r, RDIR, argv[i]);
-			}else{
-				grow(&r);
-				r.path[r.npath++] = estrdup(argv[i]);
+
+	nidx = 0;
+	idxsz = 32;
+	idx = emalloc(idxsz*sizeof(Idxent));
+	nwdir = 0;
+	wdirsz = 32;
+	wdir = emalloc(wdirsz*sizeof(Idxent));
+	argrel = emalloc(argc*sizeof(char*));
+	argn = emalloc(argc*sizeof(int));
+	for(i = 0; i < argc; i++){
+		argrel[i] = reporel(argv[i]);
+		argn[i] = strlen(argrel[i]);
+	}
+	if((o = Bfdopen(1, OWRITE)) == nil)
+		sysfatal("open out: %r");
+	if(useidx){
+		if((f = Bopen(".git/INDEX9", OREAD)) == nil){
+			fprint(2, "open index: %r\n");
+			if(access(".git/index9", AEXIST) == 0){
+				fprint(2, "index format conversion needed:\n");
+				fprint(2, "\tcd %s && git/fs\n", repopath);
+				fprint(2, "\t@{cd .git/index9/removed >[2]/dev/null && walk -f | sed 's/^/R NOQID 0 /'} >> .git/INDEX9\n");
+				fprint(2, "\t@{cd .git/fs/HEAD/tree && walk -f | sed 's/^/T NOQID 0 /'} >> .git/INDEX9\n");
 			}
-nextarg:
-			free(tpath);
-			free(rpath);
-			free(d);
+			exits("noindex");
 		}
+		line = 0;
+		while((ln = Brdstr(f, '\n', 1)) != nil){
+			line++;
+			/* allow blank lines */
+			if(ln[0] == 0 || ln[0] == '\n')
+				continue;
+			if(getfields(ln, parts, nelem(parts), 0, " \t") != nelem(parts))
+				sysfatal(".git/INDEX9:%d: corrupt index", line);
+			if(nidx == idxsz){
+				idxsz += idxsz/2;
+				idx = realloc(idx, idxsz*sizeof(Idxent));
+			}
+			cleanname(parts[3]);
+			if(strncmp(parts[3], ".git/", 5) == 0){
+				staleidx = 1;
+				free(ln);
+				continue;
+			}
+			idx[nidx].state = *parts[0];
+			idx[nidx].qid = parseqid(parts[1]);
+			idx[nidx].mode = strtol(parts[2], nil, 8);
+			idx[nidx].path = strdup(parts[3]);
+			idx[nidx].order = nidx;
+			nidx++;
+			free(ln);
+		}
+		qsort(idx, nidx, sizeof(Idxent), idxcmp);
 	}
-	dedup(&r);
 
-	for(i = 0; i < r.npath; i++){
-		p = r.path[i];
-		d = dirstat(p);
-		if(d && d->mode & DMDIR)
-			goto next;
-		rpath = smprint(RDIR"/%s", p);
-		tpath = smprint(TDIR"/%s", p);
-		bpath = smprint(HDIR"/%s", p);
-		/* Fast path: we don't want to force access to the rpath. */
-		if(d && sameqid(d, tpath)) {
-			if(!quiet && (printflg & Tflg))
-				print("%s%s\n", tstr, p);
-		}else{
-			if(d == nil || access(rpath, AEXIST) == 0 ){
-				if(access(bpath, AEXIST) == 0){
-					dirty |= Rflg;
-					if(!quiet && (printflg & Rflg))
-						print("%s%s\n", rstr, p);
+	for(i = 0; i < argc; i++){
+		argrel[i] = reporel(argv[i]);
+		argn[i] = strlen(argrel[i]);
+	}
+	if(argc == 0)
+		loadwdir(".");
+	else for(i = 0; i < argc; i++)
+		loadwdir(argrel[i]);
+	qsort(wdir, nwdir, sizeof(Idxent), idxcmp);
+	for(i = 0; i < argc; i++){
+		argrel[i] = reporel(argv[i]);
+		argn[i] = strlen(argrel[i]);
+	}
+	i = 0;
+	j = 0;
+	while(i < nidx || j < nwdir){
+		/* find the last entry we tracked for a path */
+		while(i+1 < nidx && strcmp(idx[i].path, idx[i+1].path) == 0){
+			staleidx = 1;
+			i++;
+		}
+		while(j+1 < nwdir && strcmp(wdir[j].path, wdir[j+1].path) == 0)
+			j++;
+		if(i < nidx && !pfxmatch(idx[i].path, argrel, argn, argc)){
+			i++;
+			continue;
+		}
+		if(i >= nidx)
+			c = 1;
+		else if(j >= nwdir)
+			c = -1;
+		else
+			c = strcmp(idx[i].path, wdir[j].path);
+		/* exists in both index and on disk */
+		if(c == 0){
+			if(idx[i].state == 'R'){
+				if(checkedin(&idx[i], 0))
+					show(o, Rflg, rstr, idx[i].path);
+				else{
+					idx[i].state = 'U';
+					staleidx = 1;
 				}
-			}else if(access(bpath, AEXIST) == -1) {
-				dirty |= Aflg;
-				if(!quiet && (printflg & Aflg))
-					print("%s%s\n", astr, p);
-			}else if(samedata(p, bpath)){
-				if(!quiet && (printflg & Tflg))
-					print("%s%s\n", tstr, p);
-				writeqid(d, tpath);
-			}else{
-				dirty |= Mflg;
-				if(!quiet && (printflg & Mflg))
-					print("%s%s\n", mstr, p);
-			}
+			}else if(idx[i].state == 'A' && !checkedin(&idx[i], 1))
+				show(o, Aflg, astr, idx[i].path);
+			else if(!samedata(&idx[i], &wdir[j]))
+				show(o, Mflg, mstr, idx[i].path);
+			else
+				show(o, Tflg, tstr, idx[i].path);
+			i++;
+			j++;
+		/* only exists in index */
+		}else if(c < 0){
+			if(checkedin(&idx[i], 0))
+				show(o, Rflg, rstr, idx[i].path);
+			i++;
+		/* only exists on disk */
+		}else{
+			if(!useidx && checkedin(&wdir[j], 0)){
+				if(samedata(nil, &wdir[j]))
+					show(o, Tflg, tstr, wdir[j].path);
+				else
+					show(o, Mflg, mstr, wdir[j].path);
+			}else if(printflg & Uflg && pfxmatch(idx[i].path, argrel, argn, argc))
+				show(o, Uflg, ustr, wdir[j].path);
+			j++;
 		}
-		free(rpath);
-		free(tpath);
-		free(bpath);
-next:
-		free(d);
 	}
+	Bterm(o);
+
+	if(useidx && staleidx)
+	if((wfd = create(".git/INDEX9.new", OWRITE, 0644)) != -1){
+		if((w = Bfdopen(wfd, OWRITE)) == nil){
+			close(wfd);
+			goto Nope;
+		}
+		for(i = 0; i < nidx; i++){
+			while(i+1 < nidx && strcmp(idx[i].path, idx[i+1].path) == 0)
+				i++;
+			if(idx[i].state == 'U')
+				continue;
+			Bprint(w, "%c %Q %o %s\n",
+				idx[i].state,
+				idx[i].qid, 
+				idx[i].mode,
+				idx[i].path);
+		}
+		Bterm(w);
+		nulldir(&rn);
+		rn.name = "INDEX9";
+		if(remove(".git/INDEX9") == -1)
+			goto Nope;
+		if(dirwstat(".git/INDEX9.new", &rn) == -1)
+			sysfatal("rename: %r");
+	}
+
+Nope:
 	if(!dirty)
 		exits(nil);
 
-	p = buf;
-	e = buf + sizeof(buf);
+	p = xbuf;
+	e = p + sizeof(xbuf);
 	for(i = 0; (1 << i) != Tflg; i++)
 		if(dirty & (1 << i))
-			p = seprint(p, e, "%c", "DMAT"[i]);
-	exits(buf);
+			p = seprint(p, e, "%c", "RMAUT"[i]);
+	exits(xbuf);
 }