plan9fox/sys/src/cmd/git/fs.c
Ori Bernstein 03e5d9e9e2 git/merge: preserve exec bit correctly
A while ago, qwx noticed that we clobbered the exec
bit when merging files. This is not what we want, so
we changed the operator precedence to avoid merging
dirty files implicitly.

But we do want to merge, because it's convenient for
maintaining permissions. So, instead, we should do a
3 way merge of the exec bit.

This patch does that, as well as reverting the rollback
of that change.

While we're here, we adjust the timestamps correctly
in git/branch.

This requires changes to git/fs, because without an open
handler, lib9p allows opening any file with any mode,
which confuses 'test -x'.
2022-04-16 23:53:19 +00:00

907 lines
17 KiB
C

#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include "git.h"
enum {
Qroot,
Qhead,
Qbranch,
Qcommit,
Qmsg,
Qparent,
Qtree,
Qcdata,
Qhash,
Qauthor,
Qcommitter,
Qobject,
Qctl,
Qmax,
Internal=1<<7,
};
typedef struct Gitaux Gitaux;
typedef struct Crumb Crumb;
typedef struct Cache Cache;
typedef struct Uqid Uqid;
struct Crumb {
char *name;
Object *obj;
Qid qid;
int mode;
vlong mtime;
};
struct Gitaux {
int ncrumb;
Crumb *crumb;
char *refpath;
int qdir;
/* For listing object dir */
Objlist *ols;
Object *olslast;
};
struct Uqid {
vlong uqid;
vlong ppath;
vlong oid;
int t;
int idx;
};
struct Cache {
Uqid *cache;
int n;
int max;
};
char *qroot[] = {
"HEAD",
"branch",
"object",
"ctl",
};
#define Eperm "permission denied"
#define Eexist "does not exist"
#define E2long "path too long"
#define Enodir "not a directory"
#define Erepo "unable to read repo"
#define Eobject "invalid object"
#define Egreg "wat"
#define Ebadobj "invalid object"
char gitdir[512];
char *username;
char *groupname;
char *mntpt = ".git/fs";
char **branches = nil;
Cache uqidcache[512];
vlong nextqid = Qmax;
static Object* walklink(Gitaux *, char *, int, int, int*);
vlong
qpath(Crumb *p, int idx, vlong id, vlong t)
{
int h, i;
vlong pp;
Cache *c;
Uqid *u;
pp = p ? p->qid.path : 0;
h = (pp*333 + id*7 + t) & (nelem(uqidcache) - 1);
c = &uqidcache[h];
u = c->cache;
for(i=0; i <c->n ; i++){
if(u->ppath == pp && u->oid == id && u->t == t && u->idx == idx)
return (u->uqid << 8) | t;
u++;
}
if(c->n == c->max){
c->max += c->max/2 + 1;
c->cache = erealloc(c->cache, c->max*sizeof(Uqid));
}
nextqid++;
c->cache[c->n] = (Uqid){nextqid, pp, id, t, idx};
c->n++;
return (nextqid << 8) | t;
}
static Crumb*
crumb(Gitaux *aux, int n)
{
if(n < aux->ncrumb)
return &aux->crumb[aux->ncrumb - n - 1];
return nil;
}
static void
popcrumb(Gitaux *aux)
{
Crumb *c;
if(aux->ncrumb > 1){
c = crumb(aux, 0);
free(c->name);
unref(c->obj);
aux->ncrumb--;
}
}
static vlong
branchid(Gitaux *aux, char *path)
{
int i;
for(i = 0; branches[i]; i++)
if(strcmp(path, branches[i]) == 0)
goto found;
branches = realloc(branches, sizeof(char *)*(i + 2));
branches[i] = estrdup(path);
branches[i + 1] = nil;
found:
if(aux){
if(aux->refpath)
free(aux->refpath);
aux->refpath = estrdup(branches[i]);
}
return i;
}
static void
obj2dir(Dir *d, Crumb *c, Object *o, char *name)
{
d->qid = c->qid;
d->atime = c->mtime;
d->mtime = c->mtime;
d->mode = c->mode;
d->name = estrdup9p(name);
d->uid = estrdup9p(username);
d->gid = estrdup9p(groupname);
d->muid = estrdup9p(username);
if(o->type == GBlob || o->type == GTag){
d->qid.type = 0;
d->mode &= 0777;
d->length = o->size;
}
}
static int
rootgen(int i, Dir *d, void *p)
{
Crumb *c;
c = crumb(p, 0);
if (i >= nelem(qroot))
return -1;
d->mode = 0555 | DMDIR;
d->name = estrdup9p(qroot[i]);
d->qid.vers = 0;
d->qid.type = strcmp(qroot[i], "ctl") == 0 ? 0 : QTDIR;
d->qid.path = qpath(nil, i, i, Qroot);
d->uid = estrdup9p(username);
d->gid = estrdup9p(groupname);
d->muid = estrdup9p(username);
d->mtime = c->mtime;
return 0;
}
static int
branchgen(int i, Dir *d, void *p)
{
Gitaux *aux;
Dir *refs;
Crumb *c;
int n;
aux = p;
c = crumb(aux, 0);
refs = nil;
d->qid.vers = 0;
d->qid.type = QTDIR;
d->qid.path = qpath(c, i, branchid(aux, aux->refpath), Qbranch | Internal);
d->mode = 0555 | DMDIR;
d->uid = estrdup9p(username);
d->gid = estrdup9p(groupname);
d->muid = estrdup9p(username);
d->mtime = c->mtime;
d->atime = c->mtime;
if((n = slurpdir(aux->refpath, &refs)) < 0)
return -1;
if(i < n){
d->name = estrdup9p(refs[i].name);
free(refs);
return 0;
}else{
free(refs);
return -1;
}
}
static int
gtreegen(int i, Dir *d, void *p)
{
Object *o, *l, *e;
Gitaux *aux;
Crumb *c;
int m;
aux = p;
c = crumb(aux, 0);
e = c->obj;
if(i >= e->tree->nent)
return -1;
m = e->tree->ent[i].mode;
if(e->tree->ent[i].ismod)
o = emptydir();
else if((o = readobject(e->tree->ent[i].h)) == nil)
sysfatal("could not read object %H: %r", e->tree->ent[i].h);
if(e->tree->ent[i].islink)
if((l = walklink(aux, o->data, o->size, 0, &m)) != nil)
o = l;
d->qid.vers = 0;
d->qid.type = o->type == GTree ? QTDIR : 0;
d->qid.path = qpath(c, i, o->id, aux->qdir);
d->mode = m;
d->atime = c->mtime;
d->mtime = c->mtime;
d->uid = estrdup9p(username);
d->gid = estrdup9p(groupname);
d->muid = estrdup9p(username);
d->name = estrdup9p(e->tree->ent[i].name);
d->length = o->size;
return 0;
}
static int
gcommitgen(int i, Dir *d, void *p)
{
Object *o;
Crumb *c;
c = crumb(p, 0);
o = c->obj;
d->uid = estrdup9p(username);
d->gid = estrdup9p(groupname);
d->muid = estrdup9p(username);
d->mode = 0444;
d->atime = o->commit->ctime;
d->mtime = o->commit->ctime;
d->qid.type = 0;
d->qid.vers = 0;
switch(i){
case 0:
d->mode = 0755 | DMDIR;
d->name = estrdup9p("tree");
d->qid.type = QTDIR;
d->qid.path = qpath(c, i, o->id, Qtree);
break;
case 1:
d->name = estrdup9p("parent");
d->qid.path = qpath(c, i, o->id, Qparent);
break;
case 2:
d->name = estrdup9p("msg");
d->qid.path = qpath(c, i, o->id, Qmsg);
break;
case 3:
d->name = estrdup9p("hash");
d->qid.path = qpath(c, i, o->id, Qhash);
break;
case 4:
d->name = estrdup9p("author");
d->qid.path = qpath(c, i, o->id, Qauthor);
break;
default:
return -1;
}
return 0;
}
static int
objgen(int i, Dir *d, void *p)
{
Gitaux *aux;
Object *o;
Crumb *c;
char name[64];
Objlist *ols;
Hash h;
aux = p;
c = crumb(aux, 0);
if(!aux->ols)
aux->ols = mkols();
ols = aux->ols;
o = nil;
/* We tried to sent it, but it didn't fit */
if(aux->olslast && ols->idx == i + 1){
snprint(name, sizeof(name), "%H", aux->olslast->hash);
obj2dir(d, c, aux->olslast, name);
return 0;
}
while(ols->idx <= i){
if(olsnext(ols, &h) == -1)
return -1;
if((o = readobject(h)) == nil){
fprint(2, "corrupt object %H\n", h);
return -1;
}
}
if(o != nil){
snprint(name, sizeof(name), "%H", o->hash);
obj2dir(d, c, o, name);
unref(aux->olslast);
aux->olslast = ref(o);
return 0;
}
return -1;
}
static void
objread(Req *r, Gitaux *aux)
{
Object *o;
o = crumb(aux, 0)->obj;
switch(o->type){
case GBlob:
readbuf(r, o->data, o->size);
break;
case GTag:
readbuf(r, o->data, o->size);
break;
case GTree:
dirread9p(r, gtreegen, aux);
break;
case GCommit:
dirread9p(r, gcommitgen, aux);
break;
default:
sysfatal("invalid object type %d", o->type);
}
}
static void
readcommitparent(Req *r, Object *o)
{
char *buf, *p, *e;
int i, n;
/* 40 bytes per hash, 1 per nl, 1 for terminator */
n = o->commit->nparent * (40 + 1) + 1;
buf = emalloc(n);
p = buf;
e = buf + n;
for (i = 0; i < o->commit->nparent; i++)
p = seprint(p, e, "%H\n", o->commit->parent[i]);
readbuf(r, buf, p - buf);
free(buf);
}
static void
gitattach(Req *r)
{
Gitaux *aux;
Dir *d;
if((d = dirstat(".git")) == nil)
sysfatal("git/fs: %r");
if(getwd(gitdir, sizeof(gitdir)) == nil)
sysfatal("getwd: %r");
aux = emalloc(sizeof(Gitaux));
aux->crumb = emalloc(sizeof(Crumb));
aux->crumb[0].qid = (Qid){Qroot, 0, QTDIR};
aux->crumb[0].obj = nil;
aux->crumb[0].mode = DMDIR | 0555;
aux->crumb[0].mtime = d->mtime;
aux->crumb[0].name = estrdup("/");
aux->ncrumb = 1;
r->ofcall.qid = (Qid){Qroot, 0, QTDIR};
r->fid->qid = r->ofcall.qid;
r->fid->aux = aux;
respond(r, nil);
}
static Object*
walklink(Gitaux *aux, char *link, int nlink, int ndotdot, int *mode)
{
char *p, *e, *path;
Object *o, *n;
int i;
path = emalloc(nlink + 1);
memcpy(path, link, nlink);
cleanname(path);
o = crumb(aux, ndotdot)->obj;
assert(o->type == GTree);
for(p = path; *p; p = e){
n = nil;
e = p + strcspn(p, "/");
if(*e == '/')
*e++ = '\0';
/*
* cleanname guarantees these show up at the start of the name,
* which allows trimming them from the end of the trail of crumbs
* instead of needing to keep track of full parentage.
*/
if(strcmp(p, "..") == 0)
n = crumb(aux, ++ndotdot)->obj;
else if(o->type == GTree)
for(i = 0; i < o->tree->nent; i++)
if(strcmp(o->tree->ent[i].name, p) == 0){
*mode = o->tree->ent[i].mode;
n = readobject(o->tree->ent[i].h);
break;
}
o = n;
if(o == nil)
break;
}
free(path);
return o;
}
static char *
objwalk1(Qid *q, Object *o, Crumb *p, Crumb *c, char *name, vlong qdir, Gitaux *aux)
{
Object *w, *l;
char *e;
int i, m;
w = nil;
e = nil;
if(!o)
return Eexist;
if(o->type == GTree){
q->type = 0;
for(i = 0; i < o->tree->nent; i++){
if(strcmp(o->tree->ent[i].name, name) != 0)
continue;
m = o->tree->ent[i].mode;
w = readobject(o->tree->ent[i].h);
if(!w && o->tree->ent[i].ismod)
w = emptydir();
if(w && o->tree->ent[i].islink)
if((l = walklink(aux, w->data, w->size, 1, &m)) != nil)
w = l;
if(!w)
return Ebadobj;
q->type = (w->type == GTree) ? QTDIR : 0;
q->path = qpath(c, i, w->id, qdir);
c->mode = m;
c->mode |= (w->type == GTree) ? DMDIR|0755 : 0644;
c->obj = w;
break;
}
if(!w)
e = Eexist;
}else if(o->type == GCommit){
q->type = 0;
c->mtime = o->commit->mtime;
c->mode = 0644;
assert(qdir == Qcommit || qdir == Qobject || qdir == Qtree || qdir == Qhead || qdir == Qcommitter);
if(strcmp(name, "msg") == 0)
q->path = qpath(p, 0, o->id, Qmsg);
else if(strcmp(name, "parent") == 0)
q->path = qpath(p, 1, o->id, Qparent);
else if(strcmp(name, "hash") == 0)
q->path = qpath(p, 2, o->id, Qhash);
else if(strcmp(name, "author") == 0)
q->path = qpath(p, 3, o->id, Qauthor);
else if(strcmp(name, "committer") == 0)
q->path = qpath(p, 3, o->id, Qcommitter);
else if(strcmp(name, "tree") == 0){
q->type = QTDIR;
q->path = qpath(p, 4, o->id, Qtree);
unref(c->obj);
c->mode = DMDIR | 0755;
c->obj = readobject(o->commit->tree);
if(c->obj == nil)
sysfatal("could not read object %H: %r", o->commit->tree);
}
else
e = Eexist;
}else if(o->type == GTag){
e = "tag walk unimplemented";
}
return e;
}
static Object *
readref(char *pathstr)
{
char buf[128], path[128], *p, *e;
Hash h;
int n, f;
snprint(path, sizeof(path), "%s", pathstr);
while(1){
if((f = open(path, OREAD)) == -1)
return nil;
if((n = readn(f, buf, sizeof(buf) - 1)) == -1)
return nil;
close(f);
buf[n] = 0;
if(strncmp(buf, "ref:", 4) != 0)
break;
p = buf + 4;
while(isspace(*p))
p++;
if((e = strchr(p, '\n')) != nil)
*e = 0;
snprint(path, sizeof(path), ".git/%s", p);
}
if(hparse(&h, buf) == -1)
return nil;
return readobject(h);
}
static char*
gitwalk1(Fid *fid, char *name, Qid *q)
{
char path[128];
Gitaux *aux;
Crumb *c, *o;
char *e;
Dir *d;
Hash h;
e = nil;
aux = fid->aux;
q->vers = 0;
if(strcmp(name, "..") == 0){
popcrumb(aux);
c = crumb(aux, 0);
*q = c->qid;
fid->qid = *q;
return nil;
}
aux->crumb = realloc(aux->crumb, (aux->ncrumb + 1) * sizeof(Crumb));
aux->ncrumb++;
c = crumb(aux, 0);
o = crumb(aux, 1);
memset(c, 0, sizeof(Crumb));
c->mode = o->mode;
c->mtime = o->mtime;
c->obj = o->obj ? ref(o->obj) : nil;
switch(QDIR(&fid->qid)){
case Qroot:
if(strcmp(name, "HEAD") == 0){
*q = (Qid){Qhead, 0, QTDIR};
c->mode = DMDIR | 0555;
c->obj = readref(".git/HEAD");
}else if(strcmp(name, "object") == 0){
*q = (Qid){Qobject, 0, QTDIR};
c->mode = DMDIR | 0555;
}else if(strcmp(name, "branch") == 0){
*q = (Qid){Qbranch, 0, QTDIR};
aux->refpath = estrdup(".git/refs/");
c->mode = DMDIR | 0555;
}else if(strcmp(name, "ctl") == 0){
*q = (Qid){Qctl, 0, 0};
c->mode = 0644;
}else{
e = Eexist;
}
break;
case Qbranch:
if(strcmp(aux->refpath, ".git/refs/heads") == 0 && strcmp(name, "HEAD") == 0)
snprint(path, sizeof(path), ".git/HEAD");
else
snprint(path, sizeof(path), "%s/%s", aux->refpath, name);
q->type = QTDIR;
d = dirstat(path);
if(d && d->qid.type == QTDIR)
q->path = qpath(o, Qbranch, branchid(aux, path), Qbranch);
else if(d && (c->obj = readref(path)) != nil)
q->path = qpath(o, Qbranch, c->obj->id, Qcommit);
else
e = Eexist;
free(d);
break;
case Qobject:
if(c->obj){
e = objwalk1(q, o->obj, o, c, name, Qobject, aux);
}else{
if(hparse(&h, name) == -1)
return Eobject;
if((c->obj = readobject(h)) == nil)
return Eobject;
if(c->obj->type == GBlob || c->obj->type == GTag){
c->mode = 0644;
q->type = 0;
}else{
c->mode = DMDIR | 0755;
q->type = QTDIR;
}
q->path = qpath(o, Qobject, c->obj->id, Qobject);
q->vers = 0;
}
break;
case Qhead:
e = objwalk1(q, o->obj, o, c, name, Qhead, aux);
break;
case Qcommit:
e = objwalk1(q, o->obj, o, c, name, Qcommit, aux);
break;
case Qtree:
e = objwalk1(q, o->obj, o, c, name, Qtree, aux);
break;
case Qparent:
case Qmsg:
case Qcdata:
case Qhash:
case Qauthor:
case Qcommitter:
case Qctl:
return Enodir;
default:
return Egreg;
}
c->name = estrdup(name);
c->qid = *q;
fid->qid = *q;
return e;
}
static char*
gitclone(Fid *o, Fid *n)
{
Gitaux *aux, *oaux;
int i;
oaux = o->aux;
aux = emalloc(sizeof(Gitaux));
aux->ncrumb = oaux->ncrumb;
aux->crumb = eamalloc(oaux->ncrumb, sizeof(Crumb));
for(i = 0; i < aux->ncrumb; i++){
aux->crumb[i] = oaux->crumb[i];
aux->crumb[i].name = estrdup(oaux->crumb[i].name);
if(aux->crumb[i].obj)
aux->crumb[i].obj = ref(oaux->crumb[i].obj);
}
if(oaux->refpath)
aux->refpath = strdup(oaux->refpath);
aux->qdir = oaux->qdir;
n->aux = aux;
return nil;
}
static void
gitdestroyfid(Fid *f)
{
Gitaux *aux;
int i;
if((aux = f->aux) == nil)
return;
for(i = 0; i < aux->ncrumb; i++){
if(aux->crumb[i].obj)
unref(aux->crumb[i].obj);
free(aux->crumb[i].name);
}
olsfree(aux->ols);
free(aux->refpath);
free(aux->crumb);
free(aux);
}
static char *
readctl(Req *r)
{
char data[1024], ref[512], *s, *e;
int fd, n;
if((fd = open(".git/HEAD", OREAD)) == -1)
return Erepo;
/* empty HEAD is invalid */
if((n = readn(fd, ref, sizeof(ref) - 1)) <= 0)
return Erepo;
close(fd);
s = ref;
ref[n] = 0;
if(strncmp(s, "ref:", 4) == 0)
s += 4;
while(*s == ' ' || *s == '\t')
s++;
if((e = strchr(s, '\n')) != nil)
*e = 0;
if(strstr(s, "refs/") == s)
s += strlen("refs/");
snprint(data, sizeof(data), "branch %s\nrepo %s\n", s, gitdir);
readstr(r, data);
return nil;
}
static void
gitread(Req *r)
{
char buf[256], *e;
Gitaux *aux;
Object *o;
Qid *q;
aux = r->fid->aux;
q = &r->fid->qid;
o = crumb(aux, 0)->obj;
e = nil;
switch(QDIR(q)){
case Qroot:
dirread9p(r, rootgen, aux);
break;
case Qbranch:
if(o)
objread(r, aux);
else
dirread9p(r, branchgen, aux);
break;
case Qobject:
if(o)
objread(r, aux);
else
dirread9p(r, objgen, aux);
break;
case Qmsg:
readbuf(r, o->commit->msg, o->commit->nmsg);
break;
case Qparent:
readcommitparent(r, o);
break;
case Qhash:
snprint(buf, sizeof(buf), "%H\n", o->hash);
readstr(r, buf);
break;
case Qauthor:
snprint(buf, sizeof(buf), "%s\n", o->commit->author);
readstr(r, buf);
break;
case Qcommitter:
snprint(buf, sizeof(buf), "%s\n", o->commit->committer);
readstr(r, buf);
break;
case Qctl:
e = readctl(r);
break;
case Qhead:
/* Empty repositories have no HEAD */
if(o == nil)
r->ofcall.count = 0;
else
objread(r, aux);
break;
case Qcommit:
case Qtree:
case Qcdata:
objread(r, aux);
break;
default:
e = Egreg;
}
respond(r, e);
}
static void
gitopen(Req *r)
{
Gitaux *aux;
Crumb *c;
aux = r->fid->aux;
c = crumb(aux, 0);
switch(r->ifcall.mode&3){
default:
respond(r, "botched mode");
break;
case OWRITE:
respond(r, Eperm);
break;
case OREAD:
case ORDWR:
respond(r, nil);
break;
case OEXEC:
if((c->mode & 0111) == 0)
respond(r, Eperm);
else
respond(r, nil);
break;
}
}
static void
gitstat(Req *r)
{
Gitaux *aux;
Crumb *c;
aux = r->fid->aux;
c = crumb(aux, 0);
r->d.uid = estrdup9p(username);
r->d.gid = estrdup9p(groupname);
r->d.muid = estrdup9p(username);
r->d.qid = r->fid->qid;
r->d.mtime = c->mtime;
r->d.atime = c->mtime;
r->d.mode = c->mode;
if(c->obj)
obj2dir(&r->d, c, c->obj, c->name);
else
r->d.name = estrdup9p(c->name);
respond(r, nil);
}
Srv gitsrv = {
.attach=gitattach,
.walk1=gitwalk1,
.clone=gitclone,
.open=gitopen,
.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)
{
Dir *d;
gitinit();
ARGBEGIN{
case 'd':
chatty9p++;
break;
case 'm':
mntpt = EARGF(usage());
break;
default:
usage();
break;
}ARGEND;
if(argc != 0)
usage();
if((d = dirstat(".git")) == nil)
sysfatal("dirstat .git: %r");
username = strdup(d->uid);
groupname = strdup(d->gid);
free(d);
branches = emalloc(sizeof(char*));
branches[0] = nil;
postmountsrv(&gitsrv, nil, mntpt, MCREATE);
exits(nil);
}