git-trace

romele / git-trace public

62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 52m ago

src/repo.c

#include "repo.h"

#include "auth.h"
#include "db.h"
#include "hooks.h"
#include "util.h"
#include "web.h"            /* app_ctx */

#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

int name_ok(const char *s)
{
    size_t n = strlen(s);
    if (n == 0 || n > 63)
        return 0;
    if (!isalnum((unsigned char)s[0]))
        return 0;
    for (size_t i = 0; i < n; i++) {
        unsigned char c = (unsigned char)s[i];
        if (!(isalnum(c) || c == '.' || c == '_' || c == '-'))
            return 0;
    }
    return strstr(s, "..") == NULL;
}

/* owner names: same charset as name_ok, plus "_" — the ownerless
 * (platform) namespace whose repos live at <root>/_/<name>.git */
int owner_ok(const char *s)
{
    return strcmp(s, "_") == 0 || name_ok(s);
}

int repo_path(char *out, size_t outlen,
              const char *root, const char *owner, const char *name)
{
    if (!owner_ok(owner) || !name_ok(name))
        return -1;

    char cand[PATH_MAX];
    int n = snprintf(cand, sizeof cand, "%s/%s/%s.git", root, owner, name);
    if (n < 0 || (size_t)n >= sizeof cand)
        return -1;

    char real[PATH_MAX], realroot[PATH_MAX];
    if (!realpath(cand, real) || !realpath(root, realroot))
        return -1;

    size_t rl = strlen(realroot);
    if (strncmp(real, realroot, rl) != 0 || (real[rl] != '/' && real[rl] != '\0'))
        return -1;

    n = snprintf(out, outlen, "%s", real);
    return (n < 0 || (size_t)n >= outlen) ? -1 : 0;
}

int repo_exists(const char *root, const char *owner, const char *name)
{
    char p[PATH_MAX];
    return repo_path(p, sizeof p, root, owner, name) == 0;
}

/* ' -> '\'' : embed a path inside single quotes safely */
static char *shq(const char *s, char *out, size_t n)
{
    size_t w = 0;
    for (const char *p = s; *p && w + 5 < n; p++) {
        if (*p == '\'') {
            memcpy(out + w, "'\\''", 4);
            w += 4;
        } else {
            out[w++] = *p;
        }
    }
    out[w] = '\0';
    return out;
}

int repo_install_hooks(struct app_ctx *c, const char *rdir)
{
    char bin[PATH_MAX];
    ssize_t bl = readlink("/proc/self/exe", bin, sizeof bin - 1);
    if (bl > 0)
        bin[bl] = '\0';
    else
        snprintf(bin, sizeof bin, "git-trace");

    const struct { const char *file, *body; } hh[] = {
        { "update",       UPDATE_HOOK  },
        { "post-receive", POSTRECV_HOOK },
    };
    for (size_t i = 0; i < sizeof hh / sizeof hh[0]; i++) {
        char hp[PATH_MAX];
        if (snprintf(hp, sizeof hp, "%s/hooks/%s", rdir, hh[i].file)
                >= (int)sizeof hp)
            return -1;
        FILE *f = fopen(hp, "w");
        if (!f)
            return -1;
        /* shebang must stay on line 1: emit it here plus the env the
         * hook needs, then append the static body minus its shebang */
        char qr[PATH_MAX * 2], qd[PATH_MAX * 2], qb[PATH_MAX * 2];
        fprintf(f, "#!/bin/sh\nexport GIT_TRACE_REPOS='%s'\n"
                   "export GIT_TRACE_DB='%s'\nGIT_TRACE_BIN='%s'\n",
                   shq(c->repos_root, qr, sizeof qr),
                   shq(c->db_path, qd, sizeof qd),
                   shq(bin, qb, sizeof qb));
        const char *body = strchr(hh[i].body, '\n');
        fputs(body ? body + 1 : hh[i].body, f);
        fclose(f);
        chmod(hp, 0755);
    }
    return 0;
}

/*
 * Create <repos_root>/<owner>/<name>.git on disk, seed it with
 * `git init --bare`, install the update hook, register it in the DB.
 * owner "_" = ownerless (owner_uid 0).  Shared by `mkrepo` and /new.
 */
int repo_create(struct app_ctx *c, int owner_uid, const char *owner,
                const char *name, const char *desc, int is_private)
{
    char odir[PATH_MAX];
    if (snprintf(odir, sizeof odir, "%s/%s", c->repos_root, owner)
            >= (int)sizeof odir)
        return -1;
    if (mkdir_p(odir) != 0)
        return -1;

    char rdir[PATH_MAX];
    if (snprintf(rdir, sizeof rdir, "%s/%s.git", odir, name)
            >= (int)sizeof rdir)
        return -1;

    if (access(rdir, F_OK) != 0) {
        pid_t pid = fork();
        if (pid == 0) {
            execlp("git", "git", "init", "--bare", rdir, (char *)NULL);
            _exit(127);
        }
        int st = 0;
        waitpid(pid, &st, 0);
        if (!WIFEXITED(st) || WEXITSTATUS(st) != 0)
            return -1;
    }

    if (desc) {
        char dp[PATH_MAX];
        FILE *f = NULL;
        if (snprintf(dp, sizeof dp, "%s/description", rdir)
                < (int)sizeof dp)
            f = fopen(dp, "w");
        if (f) {
            fputs(desc, f);
            fputc('\n', f);
            fclose(f);
        }
    }

    if (repo_install_hooks(c, rdir) != 0)
        return -1;
    return repo_register(c->db, owner_uid, name, !is_private, desc);
}