summaryrefslogtreecommitdiff
path: root/file
diff options
context:
space:
mode:
authorWerner Almesberger <werner@almesberger.net>2016-08-17 21:07:13 -0300
committerWerner Almesberger <werner@almesberger.net>2016-08-17 21:07:13 -0300
commit265b34fd1c1e5771adae3052b3e459d5eac597c7 (patch)
treebfd27907916e3605f2b5d9d4ae13acaaa82ec797 /file
parentbf8cc205a5a7fc21f139f23c6462519f9d4eb192 (diff)
downloadeeshow-265b34fd1c1e5771adae3052b3e459d5eac597c7.tar.gz
eeshow-265b34fd1c1e5771adae3052b3e459d5eac597c7.tar.bz2
eeshow-265b34fd1c1e5771adae3052b3e459d5eac597c7.zip
eeshow/: move file and history access to file/
Diffstat (limited to 'file')
-rw-r--r--file/file.c248
-rw-r--r--file/file.h45
-rw-r--r--file/git-file.c502
-rw-r--r--file/git-file.h45
-rw-r--r--file/git-hist.c273
-rw-r--r--file/git-hist.h44
-rw-r--r--file/git-util.c83
-rw-r--r--file/git-util.h24
8 files changed, 1264 insertions, 0 deletions
diff --git a/file/file.c b/file/file.c
new file mode 100644
index 0000000..8782e58
--- /dev/null
+++ b/file/file.c
@@ -0,0 +1,248 @@
+/*
+ * file/file.c - Open and read a file
+ *
+ * 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.
+ */
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "util.h"
+#include "diag.h"
+#include "file/git-file.h"
+#include "file/file.h"
+
+
+void *file_oid(const struct file *file)
+{
+ if (!file->vcs)
+ return NULL;
+ return vcs_git_get_oid(file->vcs);
+}
+
+
+bool file_oid_eq(const void *a, const void *b)
+{
+ /*
+ * If both a and b are NULL, we don't have revision data and thus
+ * can't tell if they're identical.
+ */
+ return a && b && vcs_git_oid_eq(a, b);
+}
+
+
+bool file_cat(const struct file *file, void *user, const char *line)
+{
+ printf("%s\n", line);
+ return 1;
+}
+
+
+char *file_graft_relative(const char *base, const char *name)
+{
+ const char *slash;
+ char *res;
+ unsigned len;
+
+ if (*name == '/')
+ return NULL;
+
+ slash = strrchr(base, '/');
+ if (!slash)
+ return NULL;
+
+ len = slash + 1 - base;
+ res = alloc_size(len + strlen(name) + 1);
+ memcpy(res, base, len);
+ strcpy(res + len, name);
+
+ return res;
+}
+
+
+static bool try_related(struct file *file)
+{
+ char *tmp;
+
+ if (!file->related)
+ return 0;
+
+ tmp = file_graft_relative(file->related->name, file->name);
+ if (!tmp)
+ return NULL;
+
+ if (*file->name == '/')
+ return 0;
+
+ file->file = fopen(tmp, "r");
+ if (!file->file) {
+ free(tmp);
+ return 0;
+ }
+
+ progress(1, "reading %s\n", tmp);
+
+ free((char *) file->name);
+ file->name = tmp;
+ return 1;
+}
+
+
+/*
+ * @@@ logic isn't quite complete yet. It should go something like this:
+ *
+ * - if there is no related item,
+ * - try file,
+ * - if there is a colon, try VCS,
+ * - give up
+ * - if there is a related item,
+ * - if it is a VCS,
+ * - try the revision matching or predating (if different repo) that of the
+ * related repo,
+ ( - fall through and try as if it was a file
+ * - try opening as file. If this fails,
+ * - if the name of the file to open is absolute, give up
+ * - try `dirname related`/file_ope_open
+ * - give up
+ *
+ * @@@ should we see if non-VCS file is in a VCS ? E.g.,
+ * /home/my-libs/foo.lib 1234:/home/my-project/foo.sch
+ * or maybe use : as indictor for VCS, i.e.,
+ * :/home/my-libs/foo.lib ...
+ *
+ * @@@ explicit revision should always win over related.
+ */
+
+static void *open_vcs(struct file *file)
+{
+ char *colon;
+
+ colon = strchr(file->name, ':');
+ if (colon) {
+ char *tmp;
+
+ tmp = stralloc(file->name);
+ tmp[colon - file->name] = 0;
+ file->vcs = vcs_git_open(tmp, colon + 1,
+ file->related ? file->related->vcs : NULL);
+ if (file->vcs) {
+ free(tmp);
+ return file->vcs;
+ }
+ progress(2, "could not open %s:%s\n", tmp, colon + 1);
+ return NULL;
+ } else {
+ file->vcs = vcs_git_open(NULL, file->name,
+ file->related ? file->related->vcs : NULL);
+ if (file->vcs)
+ return file->vcs;
+ progress(2, "could not open %s\n", file->name);
+ return NULL;
+ }
+}
+
+
+static void file_init(struct file *file, const char *name,
+ const struct file *related)
+{
+ file->name = stralloc(name);
+ file->lineno = 0;
+ file->related = related;
+ file->file = NULL;
+ file->vcs = NULL;
+}
+
+
+bool file_open(struct file *file, const char *name, const struct file *related)
+{
+ file_init(file, name, related);
+
+ if (related && related->vcs) {
+ file->vcs = open_vcs(file);
+ if (file->vcs)
+ return 1;
+ }
+
+ file->file = fopen(name, "r");
+ if (file->file) {
+ progress(1, "reading %s\n", name);
+ return 1;
+ }
+
+ if (try_related(file))
+ return 1;
+
+ if (verbose)
+ diag_perror(name);
+
+ if (!strchr(name, ':')) {
+ if (!verbose)
+ diag_perror(name);
+ goto fail;
+ }
+
+ file->vcs = open_vcs(file);
+ if (file->vcs)
+ return 1;
+
+ error("could not open %s\n", name);
+fail:
+ free((char *) file->name);
+ return 0;
+}
+
+
+bool file_open_revision(struct file *file, const char *rev, const char *name,
+ const struct file *related)
+{
+ if (!rev)
+ return file_open(file, name, related);
+
+ file_init(file, name, related);
+ file->vcs = vcs_git_open(rev, name, related ? related->vcs : NULL);
+ if (file->vcs)
+ return 1;
+ progress(2, "could not open %s at %s\n", name, rev);
+ return 0;
+}
+
+
+bool file_read(struct file *file,
+ bool (*parse)(const struct file *file, void *user, const char *line),
+ void *user)
+{
+ static char *buf = NULL;
+ static size_t n = 0;
+ char *nl;
+
+ if (file->vcs)
+ return vcs_read(file->vcs, file, parse, user);
+ while (getline(&buf, &n, file->file) > 0) {
+ nl = strchr(buf, '\n');
+ if (nl)
+ *nl = 0;
+ file->lineno++;
+ if (!parse(file, user, buf))
+ return 0;
+ }
+ return 1;
+}
+
+
+void file_close(struct file *file)
+{
+ if (file->file)
+ fclose(file->file);
+ if (file->vcs)
+ vcs_close(file->vcs);
+ free((char *) file->name);
+}
diff --git a/file/file.h b/file/file.h
new file mode 100644
index 0000000..8f6c855
--- /dev/null
+++ b/file/file.h
@@ -0,0 +1,45 @@
+/*
+ * file/file.h - Open and read a file
+ *
+ * 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.
+ */
+
+#ifndef FILE_FILE_H
+#define FILE_FILE_H
+
+#include <stdbool.h>
+#include <stdio.h>
+
+
+struct file {
+ FILE *file; /* NULL if using a version control system */
+ void *vcs; /* VCS descriptor or NULL */
+ const char *name; /* name/designator given to file_open */
+ unsigned lineno;
+ const struct file *related; /* NULL if not related to anything */
+};
+
+
+void *file_oid(const struct file *file);
+bool file_oid_eq(const void *a, const void *b);
+
+bool file_cat(const struct file *file, void *user, const char *line);
+
+char *file_graft_relative(const char *base, const char *name);
+
+bool file_open(struct file *file, const char *name,
+ const struct file *related);
+bool file_open_revision(struct file *file, const char *rev, const char *name,
+ const struct file *related);
+bool file_read(struct file *file,
+ bool (*parse)(const struct file *file, void *user, const char *line),
+ void *user);
+void file_close(struct file *file);
+
+#endif /* !FILE_FILE_H */
diff --git a/file/git-file.c b/file/git-file.c
new file mode 100644
index 0000000..6a78c9c
--- /dev/null
+++ b/file/git-file.c
@@ -0,0 +1,502 @@
+/*
+ * file/git-file.c - Open and read a file from git version control system
+ *
+ * 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 "diag.h"
+#include "file/file.h"
+#include "file/git-util.h"
+#include "file/git-file.h"
+
+
+struct vcs_git {
+ const char *name;
+ const char *revision;
+ const struct vcs_git *related;
+
+ git_repository *repo;
+ git_tree *tree;
+ git_object *obj;
+
+ const void *data;
+ unsigned size;
+};
+
+
+/* ----- OID matching ------------------------------------------------------ */
+
+
+void *vcs_git_get_oid(const void *ctx)
+{
+ const struct vcs_git *vcs_git = ctx;
+ struct git_oid *new;
+
+ new = alloc_type(git_oid);
+ git_oid_cpy(new, git_object_id(vcs_git->obj));
+ return new;
+}
+
+
+bool vcs_git_oid_eq(const void *a, const void *b)
+{
+ return !git_oid_cmp(a, b);
+}
+
+
+/* ----- Open -------------------------------------------------------------- */
+
+
+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) {
+ progress(3, "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();
+
+ fatal("%s: %s\n", git_repository_path(repo), e->message);
+ }
+
+ if (git_object_type(obj) != GIT_OBJ_COMMIT)
+ fatal("%s: not a commit\n", revision);
+ commit = (git_commit *) obj;
+
+ if (git_commit_tree(&tree, commit)) {
+ const git_error *e = giterr_last();
+
+ fatal("%s: %s\n", revision, e->message);
+ }
+
+ 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)
+ diag_pfatal(repo_dir);
+ if (!S_ISDIR(repo_st.st_mode))
+ fatal("%s: not a directory\n", repo_dir);
+
+ /* 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) {
+ progress(3, "probing \"%s\" tail \"%s\"\n", tmp, tail);
+ if (stat(tmp, &path_st) == 0)
+ break;
+ if (!tmp[1])
+ fatal("%s: cannot resolve\n", path);
+ slash = strrchr(tmp, '/');
+ if (tail != end)
+ tail[-1] = '/';
+ tail = slash + 1;
+ *slash = 0;
+ }
+
+ /* remove . and .. from tail */
+
+ progress(3, "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;
+ }
+
+ /*
+ * We have something like this:
+ * /home/repo/dead/../../foo
+ */
+ if (to == tail)
+ fatal("%s: can't climb out of dead path\n", path);
+
+ /*
+ * We have something like
+ * "foo/" -> ""
+ * or
+ * "foo/bar/" -> "foo/"
+ * where "to" points to the end.
+ */
+ to--;
+ while (to != tail && to[-1] != '/')
+ to--;
+ }
+ *to = 0;
+ progress(3, "output tail \"%s\"\n", tail);
+
+ /* resolve all symlinks */
+
+ real = realpath(tmp, NULL);
+ progress(3, "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;
+
+ progress(2, "full object path \"%s\"\n", tmp);
+
+ /* find which part of our path is inside the repo */
+
+ end = tail = strchr(tmp, 0);
+ while (1) {
+ progress(3, "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;
+
+ slash = strrchr(tmp, '/');
+
+ /* "this cannot happen" */
+ if (tail == tmp || !slash)
+ fatal("divergent paths:\nrepo \"%s\"\nobject \"%s\"\n",
+ repo_dir, tail);
+
+ if (tail != end)
+ tail[-1] = '/';
+ tail = slash + 1;
+ *slash = 0;
+ }
+
+ progress(2, "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_workdir(repo));
+ /* use workdir, not path, for submodules */
+ 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;
+
+ progress(2, "repo dir \"%s\"\n", repo_path);
+
+ canon_path = canonical_path_into_repo(repo_path, path);
+ free(repo_path);
+
+ if (git_tree_entry_bypath(&entry, tree, canon_path)) {
+ const git_error *e = giterr_last();
+
+ error("%s: %s\n", path, e->message);
+ free(canon_path);
+ return NULL;
+ }
+ free(canon_path);
+
+ return entry;
+}
+
+
+static const void *get_data(struct vcs_git *vcs_git, git_tree_entry *entry,
+ unsigned *size)
+{
+ git_repository *repo =vcs_git->repo;
+ git_object *obj;
+ git_blob *blob;
+
+ if (git_tree_entry_type(entry) != GIT_OBJ_BLOB)
+ fatal("entry is not a blob\n");
+ if (git_tree_entry_to_object(&obj, repo, entry)) {
+ const git_error *e = giterr_last();
+
+ fatal("%s\n", e->message);
+ }
+ vcs_git->obj = obj;
+
+ if (verbose > 2) {
+ git_buf buf = { 0 };
+
+ if (git_object_short_id(&buf, obj)) {
+ const git_error *e = giterr_last();
+
+ fatal("%s\n", e->message);
+ }
+ progress(3, "object %s\n", buf.ptr);
+ git_buf_free(&buf);
+ }
+ 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)(const struct file *file, void *user, const char *line),
+ void *user, const struct file *file)
+{
+ char *tmp = alloc_size(len + 1);
+ bool res;
+
+ memcpy(tmp, s, len);
+ tmp[len] = 0;
+ res = parse(file, user, tmp);
+ free(tmp);
+ return res;
+}
+
+
+static bool access_file_data(struct vcs_git *vcs_git, const char *name)
+{
+ git_tree_entry *entry;
+
+ entry = find_file(vcs_git->repo, vcs_git->tree, name);
+ if (!entry)
+ return 0;
+ progress(1, "reading %s\n", name);
+
+ vcs_git->data = get_data(vcs_git, entry, &vcs_git->size);
+ return 1;
+}
+
+
+static bool related_same_repo(struct vcs_git *vcs_git)
+{
+ const struct vcs_git *related = vcs_git->related;
+
+ vcs_git->repo = related->repo;
+ vcs_git->tree = related->tree;
+
+ return access_file_data(vcs_git, vcs_git->name);
+}
+
+
+static bool related_other_repo(struct vcs_git *vcs_git)
+{
+ static bool shown = 0;
+
+ /* @@@ find revision <= date of revision in related */
+ if (!shown)
+ warning("related_other_repo is not yet implemented\n");
+ shown = 1;
+ return 0;
+}
+
+
+static bool related_only_repo(struct vcs_git *vcs_git)
+{
+ const struct vcs_git *related = vcs_git->related;
+ char *tmp;
+
+ progress(2, "trying graft \"%s\" \"%s\"\n",
+ related->name, vcs_git->name);
+ tmp = file_graft_relative(related->name, vcs_git->name);
+ if (!tmp)
+ return 0;
+
+ vcs_git->repo = related->repo;
+ vcs_git->tree = related->tree;
+
+ if (!access_file_data(vcs_git, tmp)) {
+ free(tmp);
+ return 0;
+ }
+
+ free((char *) vcs_git->name);
+ vcs_git->name = tmp;
+
+ return 1;
+}
+
+
+static bool try_related(struct vcs_git *vcs_git)
+{
+ if (!vcs_git->related)
+ return 0;
+ if (vcs_git->revision)
+ return 0;
+
+ vcs_git->repo = select_repo(vcs_git->name);
+ if (vcs_git->repo) {
+ if (!strcmp(git_repository_path(vcs_git->related->repo),
+ git_repository_path(vcs_git->repo)))
+ return related_same_repo(vcs_git);
+ else
+ return related_other_repo(vcs_git);
+ }
+
+ return related_only_repo(vcs_git);
+}
+
+
+struct vcs_git *vcs_git_open(const char *revision, const char *name,
+ const struct vcs_git *related)
+{
+ struct vcs_git *vcs_git = alloc_type(struct vcs_git);
+
+ git_init_once();
+
+ vcs_git->name = stralloc(name);
+ vcs_git->revision = revision ? stralloc(revision) : NULL;
+ vcs_git->related = related;
+
+ if (try_related(vcs_git))
+ return vcs_git;
+
+ vcs_git->repo = select_repo(name);
+ if (!vcs_git->repo) {
+ error("%s: not found\n", name);
+ goto fail;
+ }
+ progress(2, "using repository %s\n",
+ git_repository_path(vcs_git->repo));
+
+ if (!revision)
+ revision = "HEAD";
+ vcs_git->tree = pick_revision(vcs_git->repo, revision);
+
+ if (!access_file_data(vcs_git, name))
+ goto fail;
+
+ return vcs_git;
+
+fail:
+ vcs_git_close(vcs_git);
+ return 0;
+}
+
+
+/* ----- Read -------------------------------------------------------------- */
+
+
+bool vcs_git_read(void *ctx, struct file *file,
+ bool (*parse)(const struct file *file, void *user, const char *line),
+ void *user)
+{
+ const struct vcs_git *vcs_git = ctx;
+ const char *end = vcs_git->data + vcs_git->size;
+ const char *p = vcs_git->data;
+ const char *nl;
+
+ while (p != end) {
+ nl = memchr(p, '\n', end - p);
+ file->lineno++;
+ if (!nl)
+ return send_line(p, end - p, parse, user, file);
+ if (!send_line(p, nl - p, parse, user, file))
+ return 0;
+ p = nl + 1;
+ }
+ return 1;
+}
+
+
+/* ----- Close ------------------------------------------------------------- */
+
+
+void vcs_git_close(void *ctx)
+{
+ struct vcs_git *vcs_git = ctx;
+
+ free((char *) vcs_git->name);
+ free((char *) vcs_git->revision);
+ free(vcs_git);
+}
diff --git a/file/git-file.h b/file/git-file.h
new file mode 100644
index 0000000..4369b0e
--- /dev/null
+++ b/file/git-file.h
@@ -0,0 +1,45 @@
+/*
+ * file/git-file.h - Open and read a file from git version control system
+ *
+ * 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.
+ */
+
+#ifndef FILE_GIT_FILE_H
+#define FILE_GIT_FILE_H
+
+#include <stdbool.h>
+
+
+/*
+ * future-proofing: if someone wants to add back-ends for other version control
+ * systems, the identifiers will already be there.
+ */
+
+#define vcs_open vcs_git_open
+#define vcs_read vcs_git_read
+#define vcs_close vcs_git_close
+
+
+struct vcs_git;
+struct file;
+
+
+void vcs_git_init(void);
+
+void *vcs_git_get_oid(const void *ctx); /* mallocs */
+bool vcs_git_oid_eq(const void *a, const void *b);
+
+struct vcs_git *vcs_git_open(const char *revision, const char *name,
+ const struct vcs_git *related);
+bool vcs_git_read(void *ctx, struct file *file,
+ bool (*parse)(const struct file *file, void *user, const char *line),
+ void *user);
+void vcs_git_close(void *ctx);
+
+#endif /* !FILE_GIT_FILE_H */
diff --git a/file/git-hist.c b/file/git-hist.c
new file mode 100644
index 0000000..1826b9f
--- /dev/null
+++ b/file/git-hist.c
@@ -0,0 +1,273 @@
+/*
+ * file/git-hist.c - Retrieve revision history from GIT repo
+ *
+ * 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.
+ */
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h> /* for vcs_long_for_pango */
+#include <alloca.h>
+
+#include "util.h"
+#include "diag.h"
+#include "file/git-util.h"
+#include "file/git-file.h"
+#include "file/git-hist.h"
+
+
+/*
+ * @@@ we assume to have a single head. That isn't necessarily true, since
+ * each open branch has its own head. Getting this right is for further study.
+ */
+
+
+static struct hist *new_commit(unsigned branch)
+{
+ struct hist *h;
+
+ h = alloc_type(struct hist);
+ h->commit = NULL;
+ h->branch = branch;
+ h->newer = NULL;
+ h->n_newer = 0;
+ h->older = NULL;
+ h->n_older = 0;
+ return h;
+}
+
+
+static void uplink(struct hist *down, struct hist *up)
+{
+ down->newer = realloc(down->newer,
+ sizeof(struct hist *) * (down->n_newer + 1));
+ if (!down->newer)
+ diag_pfatal("realloc");
+ down->newer[down->n_newer++] = up;
+}
+
+
+static struct hist *find_commit(struct hist *h, const git_commit *commit)
+{
+ unsigned i;
+ struct hist *found;
+
+ /*
+ * @@@ should probably use
+ * git_oid_equal(git_object_id(a), git_object_id(b))
+ */
+ if (h->commit == commit)
+ return h;
+ for (i = 0; i != h->n_older; i++) {
+ if (h->older[i]->newer[0] != h)
+ continue;
+ found = find_commit(h->older[i], commit);
+ if (found)
+ return found;
+ }
+ return NULL;
+}
+
+
+static void recurse(struct hist *h,
+ unsigned n_branches, struct hist **branches)
+{
+ unsigned n, i, j;
+ struct hist **b;
+ const git_error *e;
+
+ n = git_commit_parentcount(h->commit);
+ if (verbose > 2)
+ progress(3, "commit %p: %u + %u\n", h->commit, n_branches, n);
+
+ b = alloca(sizeof(struct hist) * (n_branches - 1 + n));
+ n_branches--;
+ memcpy(b, branches, sizeof(struct hist *) * n_branches);
+
+ h->older = alloc_type_n(struct hist *, n);
+ h->n_older = n;
+
+ for (i = 0; i != n; i++) {
+ git_commit *commit;
+ struct hist *found = NULL;
+
+ if (git_commit_parent(&commit, h->commit, i)) {
+ e = giterr_last();
+ fatal("git_commit_parent: %s\n", e->message);
+ }
+ for (j = 0; j != n_branches; j++) {
+ found = find_commit(b[j], commit);
+ if (found)
+ break;
+ }
+ if (found) {
+ uplink(found, h);
+ h->older[i] = found;
+ } else {
+ struct hist *new;
+
+ new = new_commit(n_branches);
+ new->commit = commit;
+ h->older[i] = new;
+ b[n_branches++] = new;
+ uplink(new, h);
+ recurse(new, n_branches, b);
+ }
+ }
+}
+
+
+bool vcs_git_try(const char *path)
+{
+ git_repository *repo;
+
+ git_init_once();
+
+ if (git_repository_open_ext(&repo, path,
+ GIT_REPOSITORY_OPEN_CROSS_FS, NULL))
+ return 0;
+ return !git_repository_is_empty(repo);
+}
+
+
+struct hist *vcs_git_hist(const char *path)
+{
+ struct hist *head, *dirty;
+ git_repository *repo;
+ git_oid oid;
+ const git_error *e;
+
+ head = new_commit(0);
+
+ git_init_once();
+
+ if (git_repository_open_ext(&repo, path,
+ GIT_REPOSITORY_OPEN_CROSS_FS, NULL)) {
+ e = giterr_last();
+ fatal("%s: %s\n", path, e->message);
+ }
+
+ if (git_reference_name_to_id(&oid, repo, "HEAD")) {
+ e = giterr_last();
+ fatal("%s: %s\n", git_repository_path(repo), e->message);
+ }
+
+ if (git_commit_lookup(&head->commit, repo, &oid)) {
+ e = giterr_last();
+ fatal("%s: %s\n", git_repository_path(repo), e->message);
+ }
+
+ recurse(head, 1, &head);
+
+ if (!git_repo_is_dirty(repo))
+ return head;
+
+ dirty = new_commit(0);
+ dirty->older = alloc_type(struct hist *);
+ dirty->older[0] = head;
+ dirty->n_older = 1;
+ uplink(head, dirty);
+
+ return dirty;
+}
+
+
+char *vcs_git_get_rev(struct hist *h)
+{
+ const git_oid *oid = git_commit_id(h->commit);
+ char *s = alloc_size(GIT_OID_HEXSZ + 1);
+
+ return git_oid_tostr(s, GIT_OID_HEXSZ + 1, oid);
+}
+
+
+const char *vcs_git_summary(struct hist *h)
+{
+ const char *summary;
+ const git_error *e;
+
+ if (!h->commit)
+ return "Uncommitted changes";
+ summary = git_commit_summary(h->commit);
+ if (summary)
+ return summary;
+
+ e = giterr_last();
+ fatal("git_commit_summary: %s\n", e->message);
+}
+
+
+/*
+ * @@@ This one is a bit inconvenient. It depends both on the information the
+ * VCS provides, some of which is fairly generic, but some may not be, and
+ * the very specific constraints imposed by the markup format of Pango.
+ */
+
+char *vcs_git_long_for_pango(struct hist *h,
+ char *(*formatter)(const char *fmt, ...))
+{
+ const git_error *e;
+ git_buf buf = { 0 };
+ time_t commit_time;
+ const git_signature *sig;
+ char *s;
+
+ if (!h->commit)
+ return stralloc("Uncommitted changes");
+ if (git_object_short_id(&buf, (git_object *) h->commit))
+ goto fail;
+ commit_time = git_commit_time(h->commit);
+ sig = git_commit_committer(h->commit);
+ s = formatter("<b>%s</b> %s%s &lt;%s&gt;<small>\n%s</small>",
+ buf.ptr, ctime(&commit_time), sig->name, sig->email,
+ git_commit_summary(h->commit));
+ git_buf_free(&buf);
+ return s;
+
+fail:
+ e = giterr_last();
+ fatal("vcs_git_long_for_pango: %s\n", e->message);
+}
+
+
+void hist_iterate(struct hist *h,
+ void (*fn)(void *user, struct hist *h), void *user)
+{
+ unsigned i;
+
+ fn(user, h);
+ for (i = 0; i != h->n_older; i++)
+ if (h->older[i]->newer[h->older[i]->n_newer - 1] == h)
+ hist_iterate(h->older[i], fn, user);
+}
+
+
+void dump_hist(struct hist *h)
+{
+ git_buf buf = { 0 };
+ const git_error *e;
+ unsigned i;
+
+ if (h->commit) {
+ if (git_object_short_id(&buf, (git_object *) h->commit)) {
+ e = giterr_last();
+ fatal("git_object_short_id: %s\n", e->message);
+ }
+ printf("%*s%s %s\n",
+ 2 * h->branch, "", buf.ptr, vcs_git_summary(h));
+ git_buf_free(&buf);
+ } else {
+ printf("dirty\n");
+ }
+
+ for (i = 0; i != h->n_older; i++)
+ if (h->older[i]->newer[h->older[i]->n_newer - 1] == h)
+ dump_hist(h->older[i]);
+}
diff --git a/file/git-hist.h b/file/git-hist.h
new file mode 100644
index 0000000..f528816
--- /dev/null
+++ b/file/git-hist.h
@@ -0,0 +1,44 @@
+/*
+ * file/git-hist.h - Retrieve revision history from GIT repo
+ *
+ * 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.
+ */
+
+#ifndef FILE_GIT_HIST_H
+#define FILE_GIT_HIST_H
+
+#include <stdbool.h>
+
+#include <git2.h>
+
+
+struct hist {
+ struct git_commit *commit; /* NULL if uncommitted changes */
+
+ unsigned branch; /* branch index */
+
+ struct hist **newer;
+ unsigned n_newer;
+
+ struct hist **older;
+ unsigned n_older;
+};
+
+
+bool vcs_git_try(const char *path);
+struct hist *vcs_git_hist(const char *path);
+char *vcs_git_get_rev(struct hist *h);
+const char *vcs_git_summary(struct hist *hist);
+char *vcs_git_long_for_pango(struct hist *hist,
+ char *(*formatter)(const char *fmt, ...));
+void hist_iterate(struct hist *h,
+ void (*fn)(void *user, struct hist *h), void *user);
+void dump_hist(struct hist *h);
+
+#endif /* !FILE_GIT_HIST_H */
diff --git a/file/git-util.c b/file/git-util.c
new file mode 100644
index 0000000..7abf5cf
--- /dev/null
+++ b/file/git-util.c
@@ -0,0 +1,83 @@
+/*
+ * file/git-util.c - Git utility functions
+ *
+ * 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.
+ */
+
+#include <stdbool.h>
+#include <assert.h>
+
+#include <git2.h>
+
+#include "file/git-util.h"
+
+
+/*
+ * This seems to be an efficient way for finding out if a repo is dirty.
+ *
+ * http://ben.straub.cc/2013/04/02/libgit2-checkout/
+ *
+ * References:
+ * https://libgit2.github.com/libgit2/#HEAD/group/checkout/git_checkout_index
+ * https://libgit2.github.com/libgit2/#HEAD/type/git_checkout_options
+ * https://github.com/libgit2/libgit2/blob/HEAD/include/git2/checkout.h#L251-295
+ */
+
+
+static int checkout_notify_cb(git_checkout_notify_t why,
+ const char *path, const git_diff_file *baseline,
+ const git_diff_file *target, const git_diff_file *workdir,
+ void *payload)
+{
+ bool *res = payload;
+
+ assert(why == GIT_CHECKOUT_NOTIFY_DIRTY);
+
+ *res = 1;
+ return 0;
+}
+
+
+bool git_repo_is_dirty(git_repository *repo)
+{
+ git_checkout_options opts;
+ bool res = 0;
+
+ /*
+ * Initialization with GIT_CHECKOUT_OPTIONS_INIT complains about not
+ * setting checkout_strategy. git_checkout_init_options is fine.
+ */
+ git_checkout_init_options(&opts, GIT_CHECKOUT_OPTIONS_VERSION);
+ opts.checkout_strategy = GIT_CHECKOUT_NONE;
+ /* let's be explicit about this */
+ opts.notify_flags = GIT_CHECKOUT_NOTIFY_DIRTY;
+ opts.notify_cb = checkout_notify_cb;
+ opts.notify_payload = &res;
+ git_checkout_index(repo, NULL, &opts);
+
+ return res;
+}
+
+
+/*
+ * Git documentation says that git_libgit2_init can be called more then once
+ * but doesn't quite what happens then, e.g., whether references obtained
+ * before an init (except for the first, of course) can still be used after
+ * it. So we play it safe and initialize only once.
+ */
+
+void git_init_once(void)
+{
+ static bool initialized = 0;
+
+ if (!initialized) {
+ git_libgit2_init();
+ initialized = 1;
+ }
+}
diff --git a/file/git-util.h b/file/git-util.h
new file mode 100644
index 0000000..4c7e329
--- /dev/null
+++ b/file/git-util.h
@@ -0,0 +1,24 @@
+/*
+ * file/git-util.h - Git utility functions
+ *
+ * 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.
+ */
+
+#ifndef FILE_GIT_UTIL_H
+#define FILE_GIT_UTIL_H
+
+#include <stdbool.h>
+
+#include <git2.h>
+
+
+bool git_repo_is_dirty(git_repository *repo);
+void git_init_once(void);
+
+#endif /* !FILE_GIT_UTIL_H */