Root/eeshow/file/git-file.c

1/*
2 * file/git-file.c - Open and read a file from git version control system
3 *
4 * Written 2016 by Werner Almesberger
5 * Copyright 2016 by Werner Almesberger
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 */
12
13#define _GNU_SOURCE /* for get_current_dir_name */
14#include <stdlib.h>
15#include <stdio.h>
16#include <unistd.h>
17#include <string.h>
18#include <sys/types.h>
19#include <sys/stat.h>
20
21#include <git2.h>
22
23#include "misc/util.h"
24#include "misc/diag.h"
25#include "file/file.h"
26#include "file/git-util.h"
27#include "file/git-file.h"
28
29
30struct vcs_git {
31    const char *name;
32    const char *revision;
33    const struct vcs_git *related;
34
35    git_repository *repo;
36    git_tree *tree;
37    git_object *obj;
38
39    const void *data;
40    unsigned size;
41};
42
43
44/* ----- OID matching ------------------------------------------------------ */
45
46
47void *vcs_git_get_oid(const void *ctx)
48{
49    const struct vcs_git *vcs_git = ctx;
50    struct git_oid *new;
51
52    new = alloc_type(git_oid);
53    git_oid_cpy(new, git_object_id(vcs_git->obj));
54    return new;
55}
56
57
58bool vcs_git_oid_eq(const void *a, const void *b)
59{
60    return !git_oid_cmp(a, b);
61}
62
63
64/* ----- Open -------------------------------------------------------------- */
65
66
67static git_repository *select_repo(const char *path)
68{
69    git_repository *repo = NULL;
70    char *tmp = stralloc(path);
71    char *slash;
72
73    /*
74     * If we can't find a repo, this may be due to the file or directory
75     * the path points to not existing in the currently checked-out tree.
76     * So we trim off elements until we find a repository.
77     */
78    while (1) {
79        progress(3, "trying \"%s\"", tmp);
80        if (!git_repository_open_ext(&repo, *tmp ? tmp : "/",
81            GIT_REPOSITORY_OPEN_CROSS_FS, NULL))
82            break;
83        slash = strrchr(tmp, '/');
84        if (!slash)
85            break;
86        *slash = 0;
87    }
88    free(tmp);
89    return repo;
90}
91
92
93static git_tree *pick_revision(git_repository *repo, const char *revision)
94{
95    git_commit *commit;
96    git_object *obj;
97    git_tree *tree;
98
99    if (git_revparse_single(&obj, repo, revision)) {
100        const git_error *e = giterr_last();
101
102        fatal("%s: %s\n", git_repository_path(repo), e->message);
103    }
104
105    if (git_object_type(obj) != GIT_OBJ_COMMIT)
106        fatal("%s: not a commit\n", revision);
107    commit = (git_commit *) obj;
108
109    if (git_commit_tree(&tree, commit)) {
110        const git_error *e = giterr_last();
111
112        fatal("%s: %s\n", revision, e->message);
113    }
114
115    return tree;
116}
117
118
119static char *canonical_path_into_repo(const char *repo_dir, const char *path)
120{
121    struct stat repo_st, path_st;
122    char *tmp, *tmp2, *slash, *tail, *real;
123    char *to;
124    const char *end, *from;
125
126    /* identify inode of repo root */
127
128    if (stat(repo_dir, &repo_st) < 0)
129        diag_pfatal(repo_dir);
130    if (!S_ISDIR(repo_st.st_mode))
131        fatal("%s: not a directory\n", repo_dir);
132
133    /* convert relative paths to absolute */
134
135    if (*path == '/') {
136        tmp = stralloc(path);
137    } else {
138        char *cwd = get_current_dir_name();
139
140        tmp = alloc_size(strlen(cwd) + 1 + strlen(path) + 1);
141        sprintf(tmp, "%s/%s", cwd, path);
142        free(cwd);
143    }
144
145    /* remove trailing / */
146
147    slash = strrchr(tmp, '/');
148    if (slash && slash != tmp && !slash[1])
149        *slash = 0;
150
151    /*
152     * If path does point to inexistent object, separate into the part that
153     * is valid on the current system and the tail containing dead things.
154     */
155    end = tail = strchr(tmp, 0);
156
157    while (1) {
158        progress(3, "probing \"%s\" tail \"%s\"", tmp, tail);
159        if (stat(tmp, &path_st) == 0)
160            break;
161        if (!tmp[1])
162            fatal("%s: cannot resolve\n", path);
163        slash = strrchr(tmp, '/');
164        if (tail != end)
165            tail[-1] = '/';
166        tail = slash + 1;
167        *slash = 0;
168    }
169
170    /* remove . and .. from tail */
171
172    progress(3, "input tail \"%s\"", tail);
173    from = to = tail;
174    while (1) {
175        if (!strncmp(from, "./", 2)) {
176            from += 2;
177            continue;
178        }
179        if (!strcmp(from, "."))
180            break;
181        if (strncmp(from, "../", 3) && strcmp(from, "..")) {
182            while (*from) {
183                *to++ = *from++;
184                if (from[-1] == '/')
185                    break;
186            }
187            if (!*from)
188                break;
189        }
190
191        /*
192         * We have something like this:
193         * /home/repo/dead/../../foo
194         */
195        if (to == tail)
196            fatal("%s: can't climb out of dead path\n", path);
197
198        /*
199         * We have something like
200         * "foo/" -> ""
201         * or
202         * "foo/bar/" -> "foo/"
203         * where "to" points to the end.
204         */
205        to--;
206        while (to != tail && to[-1] != '/')
207            to--;
208    }
209    *to = 0;
210    progress(3, "output tail \"%s\"", tail);
211
212    /* resolve all symlinks */
213
214    real = realpath(tmp, NULL);
215    progress(3, "realpath(\"%s\") = \"%s\"", tmp, real);
216
217    /* append tail */
218
219    if (*tail) {
220        tmp2 = alloc_size(strlen(real) + 1 + strlen(tail) + 1);
221        sprintf(tmp2, "%s/%s", real, tail);
222        free(real);
223    } else {
224        tmp2 = real;
225    }
226    free(tmp);
227    tmp = tmp2;
228
229    progress(2, "full object path \"%s\"", tmp);
230
231    /* find which part of our path is inside the repo */
232
233    end = tail = strchr(tmp, 0);
234    while (1) {
235        progress(3, "trying \"%s\" tail \"%s\"", tmp, tail);
236
237        if (stat(tmp, &path_st) == 0 &&
238            path_st.st_dev == repo_st.st_dev &&
239            path_st.st_ino == repo_st.st_ino)
240            break;
241
242        slash = strrchr(tmp, '/');
243
244        /* "this cannot happen" */
245        if (tail == tmp || !slash)
246            fatal("divergent paths:\nrepo \"%s\"\nobject \"%s\"\n",
247                repo_dir, tail);
248
249        if (tail != end)
250            tail[-1] = '/';
251        tail = slash + 1;
252        *slash = 0;
253    }
254
255    progress(2, "path in repo \"%s\"", tail);
256
257    tmp2 = stralloc(tail);
258    free(tmp);
259    return tmp2;
260}
261
262
263static git_tree_entry *find_file(git_repository *repo, git_tree *tree,
264    const char *path)
265{
266    git_tree_entry *entry;
267    char *repo_path = stralloc(git_repository_workdir(repo));
268        /* use workdir, not path, for submodules */
269    char *slash, *canon_path;
270    int len;
271
272    /* remove trailing / from repo_path */
273    slash = strrchr(repo_path, '/');
274    if (slash && slash != repo_path && !slash[1])
275        *slash = 0;
276
277    len = strlen(repo_path);
278    if (len >= 5 && !strcmp(repo_path + len - 5, "/.git"))
279        repo_path[len == 5 ? 1 : len - 5] = 0;
280
281    progress(2, "repo dir \"%s\"", repo_path);
282
283    canon_path = canonical_path_into_repo(repo_path, path);
284    free(repo_path);
285
286    if (git_tree_entry_bypath(&entry, tree, canon_path)) {
287        const git_error *e = giterr_last();
288
289        error("%s: %s", path, e->message);
290        free(canon_path);
291        return NULL;
292    }
293    free(canon_path);
294
295    return entry;
296}
297
298
299static const void *get_data(struct vcs_git *vcs_git, git_tree_entry *entry,
300    unsigned *size)
301{
302    git_repository *repo =vcs_git->repo;
303    git_object *obj;
304    git_blob *blob;
305
306    if (git_tree_entry_type(entry) != GIT_OBJ_BLOB)
307        fatal("entry is not a blob\n");
308    if (git_tree_entry_to_object(&obj, repo, entry)) {
309        const git_error *e = giterr_last();
310
311        fatal("%s\n", e->message);
312    }
313    vcs_git->obj = obj;
314
315    if (verbose > 2) {
316        git_buf buf = { 0 };
317
318        if (git_object_short_id(&buf, obj)) {
319            const git_error *e = giterr_last();
320
321            fatal("%s\n", e->message);
322        }
323        progress(3, "object %s", buf.ptr);
324        git_buf_free(&buf);
325    }
326    blob = (git_blob *) obj;
327    *size = git_blob_rawsize(blob);
328    return git_blob_rawcontent(blob);
329}
330
331
332static bool send_line(const char *s, unsigned len,
333    bool (*parse)(const struct file *file, void *user, const char *line),
334    void *user, const struct file *file)
335{
336    char *tmp = alloc_size(len + 1);
337    bool res;
338
339    memcpy(tmp, s, len);
340    tmp[len] = 0;
341    res = parse(file, user, tmp);
342    free(tmp);
343    return res;
344}
345
346
347static bool access_file_data(struct vcs_git *vcs_git, const char *name)
348{
349    git_tree_entry *entry;
350
351    entry = find_file(vcs_git->repo, vcs_git->tree, name);
352    if (!entry)
353        return 0;
354    progress(1, "reading %s", name);
355
356    vcs_git->data = get_data(vcs_git, entry, &vcs_git->size);
357    return 1;
358}
359
360
361static bool related_same_repo(struct vcs_git *vcs_git)
362{
363    const struct vcs_git *related = vcs_git->related;
364
365    vcs_git->repo = related->repo;
366    vcs_git->tree = related->tree;
367
368    return access_file_data(vcs_git, vcs_git->name);
369}
370
371
372static bool related_other_repo(struct vcs_git *vcs_git)
373{
374    static bool shown = 0;
375
376    /* @@@ find revision <= date of revision in related */
377    if (!shown)
378        warning("related_other_repo is not yet implemented");
379    shown = 1;
380    return 0;
381}
382
383
384static bool related_only_repo(struct vcs_git *vcs_git)
385{
386    const struct vcs_git *related = vcs_git->related;
387    char *tmp;
388
389    progress(2, "trying graft \"%s\" \"%s\"",
390        related->name, vcs_git->name);
391    tmp = file_graft_relative(related->name, vcs_git->name);
392    if (!tmp)
393        return 0;
394
395    /*
396     * We now have a new path, but where does it lead ? If it contains a
397     * symlink, we may end up in an entirely different repo, where new
398     * adventures await. Let's find out ...
399     */
400    vcs_git->repo = select_repo(tmp);
401    if (vcs_git->repo) {
402        free((char *) vcs_git->name);
403        vcs_git->name = tmp;
404        if (!strcmp(git_repository_path(vcs_git->related->repo),
405            git_repository_path(vcs_git->repo)))
406            return related_same_repo(vcs_git);
407        else
408            return related_other_repo(vcs_git);
409    }
410
411    vcs_git->repo = related->repo;
412    vcs_git->tree = related->tree;
413
414    if (!access_file_data(vcs_git, tmp)) {
415        free(tmp);
416        return 0;
417    }
418
419    free((char *) vcs_git->name);
420    vcs_git->name = tmp;
421
422    return 1;
423}
424
425
426static bool try_related(struct vcs_git *vcs_git)
427{
428    if (!vcs_git->related)
429        return 0;
430    if (vcs_git->revision)
431        return 0;
432
433    vcs_git->repo = select_repo(vcs_git->name);
434    if (vcs_git->repo) {
435        if (!strcmp(git_repository_path(vcs_git->related->repo),
436            git_repository_path(vcs_git->repo)))
437            return related_same_repo(vcs_git);
438        else
439            return related_other_repo(vcs_git);
440    }
441
442    return related_only_repo(vcs_git);
443}
444
445
446struct vcs_git *vcs_git_open(const char *revision, const char *name,
447    const struct vcs_git *related)
448{
449    struct vcs_git *vcs_git = alloc_type(struct vcs_git);
450
451    git_init_once();
452
453    vcs_git->name = stralloc(name);
454    vcs_git->revision = revision ? stralloc(revision) : NULL;
455    vcs_git->related = related;
456
457    if (try_related(vcs_git))
458        return vcs_git;
459
460    vcs_git->repo = select_repo(name);
461    if (!vcs_git->repo) {
462        error("%s: not found", name);
463        goto fail;
464    }
465    progress(2, "using repository %s",
466        git_repository_path(vcs_git->repo));
467
468    if (!revision)
469        revision = "HEAD";
470    vcs_git->tree = pick_revision(vcs_git->repo, revision);
471
472    if (!access_file_data(vcs_git, name))
473        goto fail;
474
475    return vcs_git;
476
477fail:
478    vcs_git_close(vcs_git);
479    return 0;
480}
481
482
483/* ----- Read -------------------------------------------------------------- */
484
485
486bool vcs_git_read(void *ctx, struct file *file,
487    bool (*parse)(const struct file *file, void *user, const char *line),
488    void *user)
489{
490    const struct vcs_git *vcs_git = ctx;
491    const char *end = vcs_git->data + vcs_git->size;
492    const char *p = vcs_git->data;
493    const char *nl;
494
495    while (p != end) {
496        nl = memchr(p, '\n', end - p);
497        file->lineno++;
498        if (!nl)
499            return send_line(p, end - p, parse, user, file);
500        if (!send_line(p, nl - p, parse, user, file))
501            return 0;
502        p = nl + 1;
503    }
504    return 1;
505}
506
507
508/* ----- Close ------------------------------------------------------------- */
509
510
511void vcs_git_close(void *ctx)
512{
513    struct vcs_git *vcs_git = ctx;
514
515    free((char *) vcs_git->name);
516    free((char *) vcs_git->revision);
517    free(vcs_git);
518}
519

Archive Download this file

Branches:
master



interactive