plan9fox/sys/src/cmd/git/serve.c
Ori Bernstein bb33663b40 git/get: keep sending what we have until we get an ack
Git9 was sloppy about telling git what commits we have.

We would list the commits at the tip of the branch, but not
walk down it, which means we would request too much data if
our local branches were ahead of the remote.

This patch changes that, sending the tips *and* the first
256 commits after them, so that git can produce a better
pack for us, with fewer redundant commits.
2022-06-11 16:36:45 +00:00

540 lines
11 KiB
C

#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);
}