src/git_repo.cpp
browsing at commit = d2d178cea97e3d1912849cb39f9d24332cce0d5d
#include "git_repo.h"
#include "util.h"
#include <git2.h>
#include <algorithm>
#include <array>
namespace {
constexpr int OID_HEX_LEN = GIT_OID_SHA1_HEXSIZE;
constexpr int SHORT_OID_LEN = 8;
constexpr int README_PREVIEW_MAX = 65536;
void check(int error, const char *what) {
if (error == 0)
return;
const git_error *e = git_error_last();
const std::string detail = e && e->message ? e->message : "unknown error";
throw GitError(std::string(what) + ": " + detail);
}
std::string oid_to_string(const git_oid *oid) {
std::array<char, OID_HEX_LEN + 1> buf{};
git_oid_tostr(buf.data(), buf.size(), oid);
return std::string(buf.data());
}
// Resolves a revision spec (or HEAD when empty) to a commit. Caller owns the
// returned commit and must free it.
git_commit *lookup_commit(git_repository *repo, const std::string &id) {
git_object *obj = nullptr;
if (id.empty()) {
git_reference *head = nullptr;
check(git_repository_head(&head, repo), "resolve HEAD");
const int err = git_reference_peel(&obj, head, GIT_OBJECT_COMMIT);
git_reference_free(head);
check(err, "peel HEAD to commit");
} else {
git_object *parsed = nullptr;
check(git_revparse_single(&parsed, repo, id.c_str()), "parse revision");
const int err = git_object_peel(&obj, parsed, GIT_OBJECT_COMMIT);
git_object_free(parsed);
check(err, "peel to commit");
}
return reinterpret_cast<git_commit *>(obj);
}
// Whether `target` is reachable from `tip` (i.e. the commit at `tip` contains
// `target` in its history, or is that commit).
bool commit_reaches(git_repository *repo, const git_oid *tip, const git_oid *target) {
if (git_oid_equal(tip, target))
return true;
return git_graph_descendant_of(repo, tip, target) == 1;
}
git_commit *lookup_commit_by_oid(git_repository *repo, const std::string &oid_hex) {
git_oid oid;
check(git_oid_fromstr(&oid, oid_hex.c_str()), "parse oid");
git_commit *commit = nullptr;
check(git_commit_lookup(&commit, repo, &oid), "lookup commit");
return commit;
}
TreeEntry make_entry(const git_tree_entry *entry, const std::string &dir,
git_repository *repo) {
TreeEntry out;
out.name = git_tree_entry_name(entry);
out.path = dir.empty() ? out.name : dir + "/" + out.name;
out.oid = oid_to_string(git_tree_entry_id(entry));
const git_object_t type = git_tree_entry_type(entry);
if (type == GIT_OBJECT_TREE) {
out.is_dir = true;
} else if (type == GIT_OBJECT_COMMIT) {
out.is_submodule = true; // gitlink
} else if (type == GIT_OBJECT_BLOB) {
git_blob *blob = nullptr;
if (git_blob_lookup(&blob, repo, git_tree_entry_id(entry)) == 0) {
out.size = static_cast<uint64_t>(git_blob_rawsize(blob));
git_blob_free(blob);
}
}
return out;
}
// Upper bound on folded path depth; guards against absurdly deep chains.
const int MAX_COLLAPSE_DEPTH = 64;
// Fold a chain of single-child directories into one entry, e.g. a "com" folder
// holding only "domain" holding only "package" becomes "com/domain/package" so
// navigation jumps straight to the deepest folder. Stops as soon as a directory
// has more than one child or its only child is not itself a directory. Costs one
// tree lookup per directory shown in a listing.
void collapse_single_child_dirs(git_repository *repo, TreeEntry &entry) {
git_oid current;
if (git_oid_fromstr(¤t, entry.oid.c_str()) != 0)
return;
for (int depth = 0; depth < MAX_COLLAPSE_DEPTH; ++depth) {
git_tree *tree = nullptr;
if (git_tree_lookup(&tree, repo, ¤t) != 0)
return;
const git_tree_entry *child =
git_tree_entrycount(tree) == 1 ? git_tree_entry_byindex(tree, 0) : nullptr;
if (!child || git_tree_entry_type(child) != GIT_OBJECT_TREE) {
git_tree_free(tree);
return;
}
entry.name += "/";
entry.name += git_tree_entry_name(child);
entry.path += "/";
entry.path += git_tree_entry_name(child);
git_oid_cpy(¤t, git_tree_entry_id(child));
entry.oid = oid_to_string(¤t);
git_tree_free(tree);
}
}
std::string first_line(const char *text) {
if (!text)
return "";
const std::string s = text;
const size_t nl = s.find('\n');
return nl == std::string::npos ? s : s.substr(0, nl);
}
char status_char(git_delta_t status) {
switch (status) {
case GIT_DELTA_ADDED: return 'A';
case GIT_DELTA_DELETED: return 'D';
case GIT_DELTA_MODIFIED: return 'M';
case GIT_DELTA_RENAMED: return 'R';
case GIT_DELTA_COPIED: return 'C';
case GIT_DELTA_TYPECHANGE: return 'T';
default: return '?';
}
}
std::string diff_css(char origin) {
switch (origin) {
case GIT_DIFF_LINE_ADDITION: return "d-add";
case GIT_DIFF_LINE_DELETION: return "d-del";
case GIT_DIFF_LINE_FILE_HDR: return "d-file";
case GIT_DIFF_LINE_HUNK_HDR: return "d-hunk";
default: return "d-ctx";
}
}
bool is_code_line(char origin) {
return origin == GIT_DIFF_LINE_CONTEXT || origin == GIT_DIFF_LINE_ADDITION ||
origin == GIT_DIFF_LINE_DELETION;
}
std::string strip_newline(const std::string &s) {
if (!s.empty() && s.back() == '\n')
return s.substr(0, s.size() - 1);
return s;
}
// Diff of a commit against its first parent (or the empty tree for a root
// commit). Returns an owned diff; caller frees. `commit` is borrowed.
git_diff *make_commit_diff(git_repository *repo, git_commit *commit) {
git_tree *new_tree = nullptr;
check(git_commit_tree(&new_tree, commit), "read commit tree");
git_tree *old_tree = nullptr;
if (git_commit_parentcount(commit) > 0) {
git_commit *parent = nullptr;
if (git_commit_parent(&parent, commit, 0) == 0) {
git_commit_tree(&old_tree, parent);
git_commit_free(parent);
}
}
git_diff *diff = nullptr;
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
const int err = git_diff_tree_to_tree(&diff, repo, old_tree, new_tree, &opts);
if (old_tree)
git_tree_free(old_tree);
git_tree_free(new_tree);
check(err, "compute diff");
return diff;
}
} // namespace
GitLibrary::GitLibrary() {
// git_libgit2_init returns the initialization count (>= 1) on success.
if (git_libgit2_init() < 0)
check(-1, "init libgit2");
// This is a read-only viewer for repositories that are mounted from
// elsewhere and are typically owned by a different user than the server
// process (e.g. root in a container). Disable libgit2's "dubious
// ownership" check, which would otherwise reject every such repository.
git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
}
GitLibrary::~GitLibrary() { git_libgit2_shutdown(); }
GitRepo::GitRepo(const std::string &path) {
check(git_repository_open(&repo_, path.c_str()), "open repository");
}
GitRepo::~GitRepo() {
if (repo_)
git_repository_free(repo_);
}
std::string GitRepo::default_branch() const {
git_reference *head = nullptr;
if (git_repository_head(&head, repo_) != 0)
return "";
const char *name = git_reference_shorthand(head);
const std::string result = name ? name : "";
git_reference_free(head);
return result;
}
std::string GitRepo::resolve_commit(const std::string &id) const {
git_commit *commit = lookup_commit(repo_, id);
const std::string oid = oid_to_string(git_commit_id(commit));
git_commit_free(commit);
return oid;
}
std::vector<TreeEntry> GitRepo::list_tree(const std::string &commit_oid,
const std::string &path) const {
git_commit *commit = lookup_commit_by_oid(repo_, commit_oid);
git_tree *root = nullptr;
const int err = git_commit_tree(&root, commit);
git_commit_free(commit);
check(err, "read commit tree");
const std::string clean = util::trim_slashes(path);
git_tree *tree = root;
git_tree *subtree = nullptr;
if (!clean.empty()) {
git_tree_entry *entry = nullptr;
if (git_tree_entry_bypath(&entry, root, clean.c_str()) != 0) {
git_tree_free(root);
throw GitError("path not found: " + clean);
}
const bool is_tree = git_tree_entry_type(entry) == GIT_OBJECT_TREE;
if (is_tree)
git_tree_entry_to_object(reinterpret_cast<git_object **>(&subtree),
repo_, entry);
git_tree_entry_free(entry);
if (!is_tree) {
git_tree_free(root);
throw GitError("not a directory: " + clean);
}
tree = subtree;
}
std::vector<TreeEntry> entries;
const size_t count = git_tree_entrycount(tree);
entries.reserve(count);
for (size_t i = 0; i < count; ++i) {
TreeEntry e = make_entry(git_tree_entry_byindex(tree, i), clean, repo_);
if (e.is_dir)
collapse_single_child_dirs(repo_, e);
entries.push_back(std::move(e));
}
if (subtree)
git_tree_free(subtree);
git_tree_free(root);
// Directories first, then files; alphabetical within each group.
std::sort(entries.begin(), entries.end(), [](const TreeEntry &a, const TreeEntry &b) {
if (a.is_dir != b.is_dir)
return a.is_dir;
return a.name < b.name;
});
return entries;
}
bool GitRepo::read_file(const std::string &commit_oid, const std::string &path,
std::string &out, bool &is_binary) const {
git_commit *commit = lookup_commit_by_oid(repo_, commit_oid);
git_tree *tree = nullptr;
const int tree_err = git_commit_tree(&tree, commit);
git_commit_free(commit);
check(tree_err, "read commit tree");
git_tree_entry *entry = nullptr;
const std::string clean = util::trim_slashes(path);
if (git_tree_entry_bypath(&entry, tree, clean.c_str()) != 0) {
git_tree_free(tree);
return false;
}
if (git_tree_entry_type(entry) != GIT_OBJECT_BLOB) {
git_tree_entry_free(entry);
git_tree_free(tree);
return false;
}
git_blob *blob = nullptr;
const int blob_err = git_blob_lookup(&blob, repo_, git_tree_entry_id(entry));
git_tree_entry_free(entry);
git_tree_free(tree);
check(blob_err, "lookup blob");
is_binary = git_blob_is_binary(blob) != 0;
out.assign(static_cast<const char *>(git_blob_rawcontent(blob)),
static_cast<size_t>(git_blob_rawsize(blob)));
git_blob_free(blob);
return true;
}
std::string GitRepo::find_readme(const std::string &commit_oid) const {
const std::vector<TreeEntry> entries = list_tree(commit_oid, "");
for (const TreeEntry &e : entries) {
if (e.is_dir)
continue;
std::string lower = e.name;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
if (lower.rfind("readme", 0) == 0)
return e.name;
}
return "";
}
std::vector<BlameLine> GitRepo::blame(const std::string &commit_oid,
const std::string &path) const {
const std::string clean = util::trim_slashes(path);
std::string content;
bool is_binary = false;
if (!read_file(commit_oid, clean, content, is_binary))
throw GitError("path not found: " + clean);
if (is_binary)
throw GitError("cannot blame binary file: " + clean);
git_blame_options opts = GIT_BLAME_OPTIONS_INIT;
check(git_oid_fromstr(&opts.newest_commit, commit_oid.c_str()), "parse oid");
git_blame *blame = nullptr;
check(git_blame_file(&blame, repo_, clean.c_str(), &opts), "compute blame");
std::vector<BlameLine> lines;
int number = 1;
size_t start = 0;
while (start <= content.size()) {
const size_t nl = content.find('\n', start);
const size_t end = nl == std::string::npos ? content.size() : nl;
BlameLine line;
line.number = number;
line.content = content.substr(start, end - start);
const git_blame_hunk *hunk = git_blame_get_hunk_byline(blame, number);
if (hunk) {
line.short_oid = oid_to_string(&hunk->final_commit_id).substr(0, SHORT_OID_LEN);
if (hunk->final_signature) {
line.author = hunk->final_signature->name;
line.time = hunk->final_signature->when.time;
}
}
lines.push_back(std::move(line));
if (nl == std::string::npos)
break;
start = nl + 1;
++number;
}
git_blame_free(blame);
return lines;
}
std::vector<CommitInfo> GitRepo::log(const std::string &start_oid, int limit) const {
git_oid oid;
check(git_oid_fromstr(&oid, start_oid.c_str()), "parse oid");
git_revwalk *walk = nullptr;
check(git_revwalk_new(&walk, repo_), "create revwalk");
git_revwalk_sorting(walk, GIT_SORT_TIME);
check(git_revwalk_push(walk, &oid), "seed revwalk");
std::vector<CommitInfo> commits;
git_oid current;
while (static_cast<int>(commits.size()) < limit &&
git_revwalk_next(¤t, walk) == 0) {
git_commit *commit = nullptr;
if (git_commit_lookup(&commit, repo_, ¤t) != 0)
continue;
CommitInfo info;
info.oid = oid_to_string(¤t);
info.short_oid = info.oid.substr(0, SHORT_OID_LEN);
const char *summary = git_commit_summary(commit);
info.summary = summary ? summary : "";
const git_signature *author = git_commit_author(commit);
if (author) {
info.author_name = author->name;
info.author_email = author->email;
info.time = author->when.time;
}
commits.push_back(std::move(info));
git_commit_free(commit);
}
git_revwalk_free(walk);
return commits;
}
std::vector<TagInfo> GitRepo::tags() const {
struct Context {
std::vector<TagInfo> *out;
git_repository *repo;
};
std::vector<TagInfo> result;
Context ctx{&result, repo_};
auto callback = [](const char *name, git_oid *oid, void *payload) -> int {
auto *c = static_cast<Context *>(payload);
TagInfo tag;
const std::string prefix = "refs/tags/";
const std::string ref = name;
tag.name = ref.rfind(prefix, 0) == 0 ? ref.substr(prefix.size()) : ref;
git_object *obj = nullptr;
if (git_object_lookup(&obj, c->repo, oid, GIT_OBJECT_ANY) != 0)
return 0;
tag.annotated = git_object_type(obj) == GIT_OBJECT_TAG;
if (tag.annotated) {
const git_signature *tagger = git_tag_tagger(reinterpret_cast<git_tag *>(obj));
if (tagger)
tag.time = tagger->when.time;
}
git_object *commit = nullptr;
if (git_object_peel(&commit, obj, GIT_OBJECT_COMMIT) == 0) {
tag.target_oid = oid_to_string(git_object_id(commit));
tag.short_oid = tag.target_oid.substr(0, SHORT_OID_LEN);
if (!tag.annotated) {
const git_signature *author =
git_commit_author(reinterpret_cast<git_commit *>(commit));
if (author)
tag.time = author->when.time;
}
git_object_free(commit);
}
git_object_free(obj);
c->out->push_back(std::move(tag));
return 0;
};
check(git_tag_foreach(repo_, callback, &ctx), "iterate tags");
std::sort(result.begin(), result.end(),
[](const TagInfo &a, const TagInfo &b) { return a.name < b.name; });
return result;
}
std::vector<std::string> GitRepo::tags_for_commit(const std::string &commit_oid) const {
std::vector<std::string> result;
for (const TagInfo &tag : tags()) {
if (tag.target_oid == commit_oid)
result.push_back(tag.name);
}
return result;
}
TagDetail GitRepo::tag_detail(const std::string &name) const {
TagDetail detail;
detail.name = name;
const std::string refname = "refs/tags/" + name;
git_reference *ref = nullptr;
if (git_reference_lookup(&ref, repo_, refname.c_str()) != 0)
throw GitError("no such tag: " + name);
git_object *obj = nullptr;
const int err = git_reference_peel(&obj, ref, GIT_OBJECT_ANY);
git_reference_free(ref);
check(err, "resolve tag");
detail.annotated = git_object_type(obj) == GIT_OBJECT_TAG;
if (detail.annotated) {
git_tag *tag = reinterpret_cast<git_tag *>(obj);
const git_signature *tagger = git_tag_tagger(tag);
if (tagger) {
detail.tagger_name = tagger->name;
detail.time = tagger->when.time;
}
const char *message = git_tag_message(tag);
detail.message = message ? message : "";
}
git_object *commit = nullptr;
if (git_object_peel(&commit, obj, GIT_OBJECT_COMMIT) == 0) {
detail.target_oid = oid_to_string(git_object_id(commit));
detail.short_oid = detail.target_oid.substr(0, SHORT_OID_LEN);
if (!detail.annotated) {
const git_signature *author =
git_commit_author(reinterpret_cast<git_commit *>(commit));
if (author)
detail.time = author->when.time;
}
git_object_free(commit);
}
git_object_free(obj);
return detail;
}
std::vector<BranchInfo> GitRepo::branches() const {
const std::string def = default_branch();
std::vector<BranchInfo> result;
git_branch_iterator *it = nullptr;
check(git_branch_iterator_new(&it, repo_, GIT_BRANCH_LOCAL), "iterate branches");
git_reference *ref = nullptr;
git_branch_t type;
while (git_branch_next(&ref, &type, it) == 0) {
BranchInfo branch;
const char *name = nullptr;
if (git_branch_name(&name, ref) == 0 && name)
branch.name = name;
branch.is_default = branch.name == def;
git_object *commit = nullptr;
if (git_reference_peel(&commit, ref, GIT_OBJECT_COMMIT) == 0) {
branch.target_oid = oid_to_string(git_object_id(commit));
branch.short_oid = branch.target_oid.substr(0, SHORT_OID_LEN);
const git_signature *author =
git_commit_author(reinterpret_cast<git_commit *>(commit));
if (author)
branch.time = author->when.time;
git_object_free(commit);
}
result.push_back(std::move(branch));
git_reference_free(ref);
}
git_branch_iterator_free(it);
std::sort(result.begin(), result.end(),
[](const BranchInfo &a, const BranchInfo &b) { return a.name < b.name; });
return result;
}
std::vector<BranchInfo> GitRepo::branches_for_commit(const std::string &commit_oid) const {
git_oid target;
check(git_oid_fromstr(&target, commit_oid.c_str()), "parse oid");
std::vector<BranchInfo> result;
for (const BranchInfo &b : branches()) {
git_oid tip;
if (git_oid_fromstr(&tip, b.target_oid.c_str()) != 0)
continue;
if (commit_reaches(repo_, &tip, &target))
result.push_back(b);
}
return result;
}
CommitDetail GitRepo::commit_detail(const std::string &oid) const {
git_commit *commit = lookup_commit(repo_, oid);
CommitDetail detail;
detail.oid = oid_to_string(git_commit_id(commit));
detail.short_oid = detail.oid.substr(0, SHORT_OID_LEN);
const git_signature *author = git_commit_author(commit);
if (author) {
detail.author_name = author->name;
if (util::show_emails())
detail.author_email = author->email;
detail.time = author->when.time;
}
const char *message = git_commit_message(commit);
detail.message = message ? message : "";
const unsigned parents = git_commit_parentcount(commit);
for (unsigned i = 0; i < parents; ++i)
detail.parents.push_back(oid_to_string(git_commit_parent_id(commit, i)));
if (!detail.parents.empty())
detail.first_parent = detail.parents.front();
git_diff *diff = make_commit_diff(repo_, commit);
const size_t deltas = git_diff_num_deltas(diff);
for (size_t i = 0; i < deltas; ++i) {
const git_diff_delta *delta = git_diff_get_delta(diff, i);
FileChange change;
change.status = status_char(delta->status);
const char *new_path = delta->new_file.path;
const char *old_path = delta->old_file.path;
change.path = new_path ? new_path : (old_path ? old_path : "");
change.old_path = old_path ? old_path : "";
detail.files.push_back(std::move(change));
}
auto line_cb = [](const git_diff_delta *, const git_diff_hunk *,
const git_diff_line *line, void *payload) -> int {
auto *lines = static_cast<std::vector<DiffLine> *>(payload);
DiffLine dl;
dl.css = diff_css(line->origin);
std::string text(line->content, line->content_len);
if (is_code_line(line->origin))
text = std::string(1, line->origin) + text;
dl.content = strip_newline(text);
lines->push_back(std::move(dl));
return 0;
};
git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, line_cb, &detail.diff);
git_diff_free(diff);
git_commit_free(commit);
return detail;
}
std::string GitRepo::commit_patch(const std::string &oid) const {
git_commit *commit = lookup_commit(repo_, oid);
git_diff *diff = make_commit_diff(repo_, commit);
auto line_cb = [](const git_diff_delta *, const git_diff_hunk *,
const git_diff_line *line, void *payload) -> int {
auto *out = static_cast<std::string *>(payload);
if (is_code_line(line->origin))
out->push_back(line->origin);
out->append(line->content, line->content_len);
return 0;
};
std::string patch;
git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, line_cb, &patch);
git_diff_free(diff);
git_commit_free(commit);
return patch;
}
ObjectView GitRepo::inspect(const std::string &oid_hex) const {
git_oid oid;
const size_t len = oid_hex.length();
check(git_oid_fromstrn(&oid, oid_hex.c_str(), len), "parse oid");
git_object *obj = nullptr;
if (len == static_cast<size_t>(OID_HEX_LEN))
check(git_object_lookup(&obj, repo_, &oid, GIT_OBJECT_ANY), "lookup object");
else
check(git_object_lookup_prefix(&obj, repo_, &oid, len, GIT_OBJECT_ANY),
"lookup object");
ObjectView view;
view.oid = oid_to_string(git_object_id(obj));
switch (git_object_type(obj)) {
case GIT_OBJECT_BLOB: {
view.kind = ObjectKind::Blob;
view.type_name = "blob";
git_blob *blob = reinterpret_cast<git_blob *>(obj);
view.is_binary = git_blob_is_binary(blob) != 0;
if (!view.is_binary) {
const auto size = static_cast<size_t>(git_blob_rawsize(blob));
const size_t take = std::min<size_t>(size, README_PREVIEW_MAX);
view.body.assign(static_cast<const char *>(git_blob_rawcontent(blob)), take);
} else {
view.body = "binary blob (" +
util::human_size(static_cast<uint64_t>(git_blob_rawsize(blob))) +
")";
}
break;
}
case GIT_OBJECT_TREE: {
view.kind = ObjectKind::Tree;
view.type_name = "tree";
git_tree *tree = reinterpret_cast<git_tree *>(obj);
const size_t count = git_tree_entrycount(tree);
for (size_t i = 0; i < count; ++i)
view.entries.push_back(make_entry(git_tree_entry_byindex(tree, i), "", repo_));
break;
}
case GIT_OBJECT_COMMIT: {
view.kind = ObjectKind::Commit;
view.type_name = "commit";
git_commit *commit = reinterpret_cast<git_commit *>(obj);
const git_signature *author = git_commit_author(commit);
std::string text = "commit " + view.oid + "\n";
if (author) {
text += "Author: " + std::string(author->name);
if (util::show_emails())
text += " <" + std::string(author->email) + ">";
text += "\nDate: " + util::format_time(author->when.time) + "\n";
}
const char *message = git_commit_message(commit);
text += "\n";
text += message ? message : "";
view.body = text;
break;
}
case GIT_OBJECT_TAG: {
view.kind = ObjectKind::Tag;
view.type_name = "tag";
git_tag *tag = reinterpret_cast<git_tag *>(obj);
const char *name = git_tag_name(tag);
const char *message = git_tag_message(tag);
view.body = "tag " + std::string(name ? name : "") + "\n\n" +
std::string(message ? message : "");
break;
}
default:
view.kind = ObjectKind::Unknown;
view.type_name = "unknown";
break;
}
git_object_free(obj);
return view;
}