summaryrefslogtreecommitdiff
path: root/git-file.c
diff options
context:
space:
mode:
Diffstat (limited to 'git-file.c')
-rw-r--r--git-file.c371
1 files changed, 371 insertions, 0 deletions
diff --git a/git-file.c b/git-file.c
new file mode 100644
index 0000000..098f136
--- /dev/null
+++ b/git-file.c
@@ -0,0 +1,371 @@
+/*
+ * git-file.c - Open and read a file from git
+ *
+ * Written 2016 by Werner Almesberger
+ * Copyright 2016 by Werner Almesberger
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#define _GNU_SOURCE /* for get_current_dir_name */
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <git2.h>
+
+#include "util.h"
+#include "main.h"
+#include "git-file.h"
+
+
+static git_repository *select_repo(const char *path)
+{
+ git_repository *repo = NULL;
+ char *tmp = stralloc(path);
+ char *slash;
+
+ /*
+ * If we can't find a repo, this may be due to the file or directory
+ * the path points to not existing in the currently checked-out tree.
+ * So we trim off elements until we find a repository.
+ */
+ while (1) {
+ if (verbose > 2)
+ fprintf(stderr, "trying \"%s\"\n", tmp);
+ if (!git_repository_open_ext(&repo, *tmp ? tmp : "/",
+ GIT_REPOSITORY_OPEN_CROSS_FS, NULL))
+ break;
+ slash = strrchr(tmp, '/');
+ if (!slash)
+ break;
+ *slash = 0;
+ }
+ free(tmp);
+ return repo;
+}
+
+
+static git_tree *pick_revision(git_repository *repo, const char *revision)
+{
+ git_commit *commit;
+ git_object *obj;
+ git_tree *tree;
+
+ if (git_revparse_single(&obj, repo, revision)) {
+ const git_error *e = giterr_last();
+
+ fprintf(stderr, "%s: %s\n",
+ git_repository_path(repo), e->message);
+ exit(1);
+ }
+
+ if (git_object_type(obj) != GIT_OBJ_COMMIT) {
+ fprintf(stderr, "%s: not a commit\n", revision);
+ exit(1);
+ }
+ commit = (git_commit *) obj;
+
+ if (git_commit_tree(&tree, commit)) {
+ const git_error *e = giterr_last();
+
+ fprintf(stderr, "%s: %s\n", revision, e->message);
+ exit(1);
+ }
+
+ return tree;
+}
+
+
+static char *canonical_path_into_repo(const char *repo_dir, const char *path)
+{
+ struct stat repo_st, path_st;
+ char *tmp, *tmp2, *slash, *tail, *real;
+ char *to;
+ const char *end, *from;
+
+ /* identify inode of repo root */
+
+ if (stat(repo_dir, &repo_st) < 0) {
+ perror(repo_dir);
+ exit(1);
+ }
+ if (!S_ISDIR(repo_st.st_mode)) {
+ fprintf(stderr, "%s: not a directory\n", repo_dir);
+ exit(1);
+ }
+
+ /* convert relative paths to absolute */
+
+ if (*path == '/') {
+ tmp = stralloc(path);
+ } else {
+ char *cwd = get_current_dir_name();
+
+ tmp = alloc_size(strlen(cwd) + 1 + strlen(path) + 1);
+ sprintf(tmp, "%s/%s", cwd, path);
+ free(cwd);
+ }
+
+ /* remove trailing / */
+
+ slash = strrchr(tmp, '/');
+ if (slash && slash != tmp && !slash[1])
+ *slash = 0;
+
+
+ /*
+ * If path does point to inexistent object, separate into the part that
+ * is valid on the current system and the tail containing dead things.
+ */
+ end = tail = strchr(tmp, 0);
+
+ while (1) {
+ if (verbose > 2)
+ fprintf(stderr, "probing \"%s\" tail \"%s\"\n",
+ tmp, tail);
+ if (stat(tmp, &path_st) == 0)
+ break;
+ if (!tmp[1]) {
+ fprintf(stderr, "%s: cannot resolve\n", path);
+ exit(1);
+ }
+ slash = strrchr(tmp, '/');
+ if (tail != end)
+ tail[-1] = '/';
+ tail = slash + 1;
+ *slash = 0;
+ }
+
+ /* remove . and .. from tail */
+
+ if (verbose > 2)
+ fprintf(stderr, "input tail \"%s\"\n", tail);
+ from = to = tail;
+ while (1) {
+ if (!strncmp(from, "./", 2)) {
+ from += 2;
+ continue;
+ }
+ if (!strcmp(from, "."))
+ break;
+ if (strncmp(from, "../", 3) && strcmp(from, "..")) {
+ while (*from) {
+ *to++ = *from++;
+ if (from[-1] == '/')
+ break;
+ }
+ if (!*from)
+ break;
+ }
+ if (to == tail) {
+ /*
+ * We have something like this:
+ * /home/repo/dead/../../foo
+ */
+ fprintf(stderr, "%s: can't climb out of dead path\n",
+ path);
+ exit(1);
+ }
+
+ /*
+ * We have something like
+ * "foo/" -> ""
+ * or
+ * "foo/bar/" -> "foo/"
+ * where "to" points to the end.
+ */
+ to--;
+ while (to != tail && to[-1] != '/')
+ to--;
+ }
+ *to = 0;
+ if (verbose > 2)
+ fprintf(stderr, "output tail \"%s\"\n", tail);
+
+ /* resolve all symlinks */
+
+ real = realpath(tmp, NULL);
+ if (verbose > 2)
+ fprintf(stderr, "realpath(\"%s\") = \"%s\"\n", tmp, real);
+
+ /* append tail */
+
+ if (*tail) {
+ tmp2 = alloc_size(strlen(real) + 1 + strlen(tail) + 1);
+ sprintf(tmp2, "%s/%s", real, tail);
+ free(real);
+ } else {
+ tmp2 = real;
+ }
+ free(tmp);
+ tmp = tmp2;
+
+ if (verbose > 1)
+ fprintf(stderr, "full object path \"%s\"\n", tmp);
+
+ /* find which part of our path is inside the repo */
+
+ end = tail = strchr(tmp, 0);
+ while (1) {
+ if (verbose > 2)
+ fprintf(stderr, "trying \"%s\" tail \"%s\"\n",
+ tmp, tail);
+
+ if (stat(tmp, &path_st) == 0 &&
+ path_st.st_dev == repo_st.st_dev &&
+ path_st.st_ino == repo_st.st_ino)
+ break;
+
+ /* "this cannot happen" */
+ if (tail == tmp) {
+ fprintf(stderr,
+ "divergent paths:\nrepo \"%s\"\nobject \"%s\"\n",
+ repo_dir, tmp);
+ exit(1);
+ }
+
+ slash = strrchr(tmp, '/');
+ if (tail != end)
+ tail[-1] = '/';
+ tail = slash + 1;
+ *slash = 0;
+ }
+
+ if (verbose > 1)
+ fprintf(stderr, "path in repo \"%s\"\n", tail);
+
+ tmp2 = stralloc(tail);
+ free(tmp);
+ return tmp2;
+}
+
+
+static git_tree_entry *find_file(git_repository *repo, git_tree *tree,
+ const char *path)
+{
+ git_tree_entry *entry;
+ char *repo_path = stralloc(git_repository_path(repo));
+ char *slash, *canon_path;
+ int len;
+
+ /* remove trailing / from repo_path */
+ slash = strrchr(repo_path, '/');
+ if (slash && slash != repo_path && !slash[1])
+ *slash = 0;
+
+ len = strlen(repo_path);
+ if (len >= 5 && !strcmp(repo_path + len - 5, "/.git"))
+ repo_path[len == 5 ? 1 : len - 5] = 0;
+
+ if (verbose > 1)
+ fprintf(stderr, "repo dir \"%s\"\n", repo_path);
+
+ canon_path = canonical_path_into_repo(repo_path, path);
+
+ if (git_tree_entry_bypath(&entry, tree, canon_path)) {
+ const git_error *e = giterr_last();
+
+ fprintf(stderr, "%s: %s\n", path, e->message);
+ exit(1);
+ }
+ free(canon_path);
+
+ return entry;
+}
+
+
+static const void *get_data(git_repository *repo, git_tree_entry *entry,
+ unsigned *size)
+{
+ git_object *obj;
+ git_blob *blob;
+
+ if (git_tree_entry_type(entry) != GIT_OBJ_BLOB) {
+ fprintf(stderr, "entry is not a blob\n");
+ exit(1);
+ }
+ if (git_tree_entry_to_object(&obj, repo, entry)) {
+ const git_error *e = giterr_last();
+
+ fprintf(stderr, "%s\n", e->message);
+ exit(1);
+ }
+
+ blob = (git_blob *) obj;
+ *size = git_blob_rawsize(blob);
+ return git_blob_rawcontent(blob);
+}
+
+
+static bool send_line(const char *s, unsigned len,
+ bool (*parse)(void *user, const char *line), void *user)
+{
+ char *tmp = alloc_size(len + 1);
+ bool res;
+
+ memcpy(tmp, s, len);
+ tmp[len] = 0;
+ res = parse(user, tmp);
+ free(tmp);
+ return res;
+}
+
+
+static void send_data(const char *data, unsigned size,
+ bool (*parse)(void *user, const char *line), void *user)
+{
+ const char *end = data + size;
+ const char *p = data;
+ const char *nl;
+
+ while (p != end) {
+ nl = memchr(p, '\n', end - p);
+ if (!nl) {
+ send_line(p, end - p, parse, user);
+ return;
+ }
+ if (!send_line(p, nl - p, parse, user))
+ return;
+ p = nl + 1;
+ }
+}
+
+
+void git_read(const char *revision, const char *name,
+ bool (*parse)(void *user, const char *line), void *user)
+{
+ static bool initialized = 0;
+ git_repository *repo;
+ git_tree *tree;
+ git_tree_entry *entry;
+ const void *data;
+ unsigned size;
+
+ if (!initialized) {
+ git_libgit2_init();
+ initialized = 1;
+ }
+
+ repo = select_repo(name);
+ if (!repo) {
+ fprintf(stderr, "%s:%s not found\n", revision, name);
+ exit(1);
+ }
+ if (verbose > 1)
+ fprintf(stderr, "using repository %s\n",
+ git_repository_path(repo));
+
+ tree = pick_revision(repo, revision);
+ entry = find_file(repo, tree, name);
+ if (verbose)
+ fprintf(stderr, "reading %s:%s\n", revision, name);
+ data = get_data(repo, entry, &size);
+ send_data(data, size, parse, user);
+}