#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <auth.h>
#include "git.h"
char *pathpfx = nil;
int allowwrite;
int
showrefs(Conn *c)
{
int i, ret, nrefs;
Hash head, *refs;
char **names;
ret = -1;
nrefs = 0;
refs = nil;
names = nil;
if(resolveref(&head, "HEAD") != -1)
if(fmtpkt(c, "%H HEAD\n", head) == -1)
goto error;
if((nrefs = listrefs(&refs, &names)) == -1)
sysfatal("listrefs: %r");
for(i = 0; i < nrefs; i++){
if(strncmp(names[i], "heads/", strlen("heads/")) != 0)
continue;
if(fmtpkt(c, "%H refs/%s\n", refs[i], names[i]) == -1)
goto error;
}
if(flushpkt(c) == -1)
goto error;
ret = 0;
error:
for(i = 0; i < nrefs; i++)
free(names[i]);
free(names);
free(refs);
return ret;
}
int
servnegotiate(Conn *c, Hash **head, int *nhead, Hash **tail, int *ntail)
{
char pkt[Pktmax];
int n, acked;
Object *o;
Hash h;
if(showrefs(c) == -1)
return -1;
*head = nil;
*tail = nil;
*nhead = 0;
*ntail = 0;
while(1){
if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
goto error;
if(n == 0)
break;
if(strncmp(pkt, "want ", 5) != 0){
werrstr(" protocol garble %s", pkt);
goto error;
}
if(hparse(&h, &pkt[5]) == -1){
werrstr(" garbled want");
goto error;
}
if((o = readobject(h)) == nil){
werrstr("requested nonexistent object");
goto error;
}
unref(o);
*head = erealloc(*head, (*nhead + 1)*sizeof(Hash));
(*head)[*nhead] = h;
*nhead += 1;
}
acked = 0;
while(1){
if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
goto error;
if(strncmp(pkt, "done", 4) == 0)
break;
if(n == 0){
if(!acked && fmtpkt(c, "NAK") == -1)
goto error;
}
if(strncmp(pkt, "have ", 5) != 0){
werrstr(" protocol garble %s", pkt);
goto error;
}
if(hparse(&h, &pkt[5]) == -1){
werrstr(" garbled have");
goto error;
}
if((o = readobject(h)) == nil)
continue;
if(!acked){
if(fmtpkt(c, "ACK %H", h) == -1)
goto error;
acked = 1;
}
unref(o);
*tail = erealloc(*tail, (*ntail + 1)*sizeof(Hash));
(*tail)[*ntail] = h;
*ntail += 1;
}
if(!acked && fmtpkt(c, "NAK\n") == -1)
goto error;
return 0;
error:
fmtpkt(c, "ERR %r\n");
free(*head);
free(*tail);
return -1;
}
int
servpack(Conn *c)
{
Hash *head, *tail, h;
int nhead, ntail;
dprint(1, "negotiating pack\n");
if(servnegotiate(c, &head, &nhead, &tail, &ntail) == -1)
sysfatal("negotiate: %r");
dprint(1, "writing pack\n");
if(writepack(c->wfd, head, nhead, tail, ntail, &h) == -1)
sysfatal("send: %r");
return 0;
}
int
validref(char *s)
{
if(strncmp(s, "refs/", 5) != 0)
return 0;
for(; *s != '\0'; s++)
if(!isalnum(*s) && strchr("/-_.", *s) == nil)
return 0;
return 1;
}
int
recvnegotiate(Conn *c, Hash **cur, Hash **upd, char ***ref, int *nupd)
{
char pkt[Pktmax], *sp[4];
Hash old, new;
int n, i;
if(showrefs(c) == -1)
return -1;
*cur = nil;
*upd = nil;
*ref = nil;
*nupd = 0;
while(1){
if((n = readpkt(c, pkt, sizeof(pkt))) == -1)
goto error;
if(n == 0)
break;
if(getfields(pkt, sp, nelem(sp), 1, " \t\n\r") != 3){
fmtpkt(c, "ERR protocol garble %s\n", pkt);
goto error;
}
if(hparse(&old, sp[0]) == -1){
fmtpkt(c, "ERR bad old hash %s\n", sp[0]);
goto error;
}
if(hparse(&new, sp[1]) == -1){
fmtpkt(c, "ERR bad new hash %s\n", sp[1]);
goto error;
}
if(!validref(sp[2])){
fmtpkt(c, "ERR invalid ref %s\n", sp[2]);
goto error;
}
*cur = erealloc(*cur, (*nupd + 1)*sizeof(Hash));
*upd = erealloc(*upd, (*nupd + 1)*sizeof(Hash));
*ref = erealloc(*ref, (*nupd + 1)*sizeof(Hash));
(*cur)[*nupd] = old;
(*upd)[*nupd] = new;
(*ref)[*nupd] = estrdup(sp[2]);
*nupd += 1;
}
return 0;
error:
free(*cur);
free(*upd);
for(i = 0; i < *nupd; i++)
free((*ref)[i]);
free(*ref);
return -1;
}
int
rename(char *pack, char *idx, Hash h)
{
char name[128], path[196];
Dir st;
nulldir(&st);
st.name = name;
snprint(name, sizeof(name), "%H.pack", h);
snprint(path, sizeof(path), ".git/objects/pack/%s", name);
if(access(path, AEXIST) == 0)
fprint(2, "warning, pack %s already pushed\n", name);
else if(dirwstat(pack, &st) == -1)
return -1;
snprint(name, sizeof(name), "%H.idx", h);
snprint(path, sizeof(path), ".git/objects/pack/%s", name);
if(access(path, AEXIST) == 0)
fprint(2, "warning, pack %s already indexed\n", name);
else if(dirwstat(idx, &st) == -1)
return -1;
return 0;
}
int
checkhash(int fd, vlong sz, Hash *hcomp)
{
DigestState *st;
Hash hexpect;
char buf[Pktmax];
vlong n, r;
int nr;
if(sz < 28){
werrstr("undersize packfile");
return -1;
}
st = nil;
n = 0;
if(seek(fd, 0, 0) == -1)
sysfatal("packfile seek: %r");
while(n != sz - 20){
nr = sizeof(buf);
if(sz - n - 20 < sizeof(buf))
nr = sz - n - 20;
r = readn(fd, buf, nr);
if(r != nr){
werrstr("short read");
return -1;
}
st = sha1((uchar*)buf, nr, nil, st);
n += r;
}
sha1(nil, 0, hcomp->h, st);
if(readn(fd, hexpect.h, sizeof(hexpect.h)) != sizeof(hexpect.h))
sysfatal("truncated packfile");
if(!hasheq(hcomp, &hexpect)){
werrstr("bad hash: %H != %H", *hcomp, hexpect);
return -1;
}
return 0;
}
int
mkdir(char *dir)
{
char buf[ERRMAX];
int f;
if(access(dir, AEXIST) == 0)
return 0;
if((f = create(dir, OREAD, DMDIR | 0755)) == -1){
rerrstr(buf, sizeof(buf));
if(strstr(buf, "exist") == nil)
return -1;
}
close(f);
return 0;
}
int
updatepack(Conn *c)
{
char buf[Pktmax], packtmp[128], idxtmp[128], ebuf[ERRMAX];
int n, pfd, packsz;
Hash h;
/* make sure the needed dirs exist */
if(mkdir(".git/objects") == -1)
return -1;
if(mkdir(".git/objects/pack") == -1)
return -1;
if(mkdir(".git/refs") == -1)
return -1;
if(mkdir(".git/refs/heads") == -1)
return -1;
snprint(packtmp, sizeof(packtmp), ".git/objects/pack/recv-%d.pack.tmp", getpid());
snprint(idxtmp, sizeof(idxtmp), ".git/objects/pack/recv-%d.idx.tmp", getpid());
if((pfd = create(packtmp, ORDWR, 0644)) == -1)
return -1;
packsz = 0;
while(1){
n = read(c->rfd, buf, sizeof(buf));
if(n == 0)
break;
if(n == -1){
rerrstr(ebuf, sizeof(ebuf));
if(strstr(ebuf, "hungup") == nil)
return -1;
break;
}
if(write(pfd, buf, n) != n)
return -1;
packsz += n;
}
if(checkhash(pfd, packsz, &h) == -1){
dprint(1, "hash mismatch\n");
goto error1;
}
if(indexpack(packtmp, idxtmp, h) == -1){
dprint(1, "indexing failed: %r\n");
goto error1;
}
if(rename(packtmp, idxtmp, h) == -1){
dprint(1, "rename failed: %r\n");
goto error2;
}
return 0;
error2: remove(idxtmp);
error1: remove(packtmp);
return -1;
}
int
lockrepo(void)
{
int fd, i;
for(i = 0; i < 10; i++) {
if((fd = create(".git/_lock", ORCLOSE|ORDWR|OTRUNC|OEXCL, 0644))!= -1)
return fd;
sleep(250);
}
return -1;
}
int
updaterefs(Conn *c, Hash *cur, Hash *upd, char **ref, int nupd)
{
char refpath[512], buf[128];
int i, newidx, hadref, fd, ret, lockfd;
vlong newtm;
Object *o;
Hash h;
ret = -1;
hadref = 0;
newidx = -1;
/*
* Date of Magna Carta.
* Wrong because it was computed using
* the proleptic gregorian calendar.
*/
newtm = -23811206400;
if((lockfd = lockrepo()) == -1){
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])){
snprint(buf, sizeof(buf), "old ref changed: %s", ref[i]);
goto error;
}
}
if(snprint(refpath, sizeof(refpath), ".git/%s", ref[i]) == sizeof(refpath)){
snprint(buf, sizeof(buf), "ref path too long: %s", ref[i]);
goto error;
}
if(hasheq(&upd[i], &Zhash)){
remove(refpath);
continue;
}
if((o = readobject(upd[i])) == nil){
snprint(buf, sizeof(buf), "update to nonexistent hash %H", upd[i]);
goto error;
}
if(o->type != GCommit){
snprint(buf, sizeof(buf), "not commit: %H", upd[i]);
goto error;
}
if(o->commit->mtime > newtm){
newtm = o->commit->mtime;
newidx = i;
}
unref(o);
if((fd = create(refpath, OWRITE|OTRUNC, 0644)) == -1){
snprint(buf, sizeof(buf), "open ref: %r");
goto error;
}
if(fprint(fd, "%H", upd[i]) == -1){
snprint(buf, sizeof(buf), "upate ref: %r");
close(fd);
goto error;
}
close(fd);
}
/*
* Heuristic:
* If there are no valid refs, and HEAD is invalid, then
* pick the ref with the newest commits as the default
* branch.
*
* Several people have been caught out by pushing to
* a repo where HEAD named differently from what got
* pushed, and this is going to be more of a footgun
* when 'master', 'main', and 'front' are all in active
* use. This should make us pick a useful default in
* those cases, instead of silently failing.
*/
if(resolveref(&h, "HEAD") == -1 && hadref == 0 && newidx != -1){
if((fd = create(".git/HEAD", OWRITE|OTRUNC, 0644)) == -1){
snprint(buf, sizeof(buf), "open HEAD: %r");
goto error;
}
if(fprint(fd, "ref: %s", ref[0]) == -1){
snprint(buf, sizeof(buf), "write HEAD ref: %r");
goto error;
}
close(fd);
}
ret = 0;
error:
fmtpkt(c, "ERR %s", buf);
close(lockfd);
werrstr(buf);
return ret;
}
int
recvpack(Conn *c)
{
Hash *cur, *upd;
char **ref;
int nupd;
if(recvnegotiate(c, &cur, &upd, &ref, &nupd) == -1)
sysfatal("negotiate refs: %r");
if(nupd != 0 && updatepack(c) == -1)
sysfatal("update pack: %r");
if(nupd != 0 && updaterefs(c, cur, upd, ref, nupd) == -1)
sysfatal("update refs: %r");
return 0;
}
char*
parsecmd(char *buf, char *cmd, int ncmd)
{
int i;
char *p;
for(p = buf, i = 0; *p && i < ncmd - 1; i++, p++){
if(*p == ' ' || *p == '\t'){
cmd[i] = 0;
break;
}
cmd[i] = *p;
}
while(*p == ' ' || *p == '\t')
p++;
return p;
}
void
usage(void)
{
fprint(2, "usage: %s [-dw] [-r rel]\n", argv0);
exits("usage");
}
void
main(int argc, char **argv)
{
char *repo, cmd[32], buf[512];
Conn c;
ARGBEGIN{
case 'd':
chattygit++;
break;
case 'r':
pathpfx = EARGF(usage());
if(*pathpfx != '/')
sysfatal("path prefix must begin with '/'");
break;
case 'w':
allowwrite++;
break;
default:
usage();
break;
}ARGEND;
gitinit();
interactive = 0;
if(rfork(RFNAMEG) == -1)
sysfatal("rfork: %r");
if(pathpfx != nil){
if(bind(pathpfx, "/", MREPL) == -1)
sysfatal("bind: %r");
}
if(rfork(RFNOMNT) == -1)
sysfatal("rfork: %r");
initconn(&c, 0, 1);
if(readpkt(&c, buf, sizeof(buf)) == -1)
sysfatal("readpkt: %r");
repo = parsecmd(buf, cmd, sizeof(cmd));
cleanname(repo);
if(strncmp(repo, "../", 3) == 0)
sysfatal("invalid path %s\n", repo);
if(bind(repo, "/", MREPL) == -1){
fmtpkt(&c, "ERR no repo %r\n");
sysfatal("enter %s: %r", repo);
}
if(chdir("/") == -1)
sysfatal("chdir: %r");
if(access(".git", AREAD) == -1)
sysfatal("no git repository");
if(strcmp(cmd, "git-receive-pack") == 0 && allowwrite)
recvpack(&c);
else if(strcmp(cmd, "git-upload-pack") == 0)
servpack(&c);
else
sysfatal("unsupported command '%s'", cmd);
exits(nil);
}
|