#include "git_repo.h" #include "util.h" #include #include #include 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 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(obj); } 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(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 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(&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 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(git_blob_rawcontent(blob)), static_cast(git_blob_rawsize(blob))); git_blob_free(blob); return true; } std::string GitRepo::find_readme(const std::string &commit_oid) const { const std::vector 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 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 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 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 commits; git_oid current; while (static_cast(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 GitRepo::tags() const { struct Context { std::vector *out; git_repository *repo; }; std::vector result; Context ctx{&result, repo_}; auto callback = [](const char *name, git_oid *oid, void *payload) -> int { auto *c = static_cast(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(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(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; } 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(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(commit)); if (author) detail.time = author->when.time; } git_object_free(commit); } git_object_free(obj); return detail; } 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 *>(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(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(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(obj); view.is_binary = git_blob_is_binary(blob) != 0; if (!view.is_binary) { const auto size = static_cast(git_blob_rawsize(blob)); const size_t take = std::min(size, README_PREVIEW_MAX); view.body.assign(static_cast(git_blob_rawcontent(blob)), take); } else { view.body = "binary blob (" + util::human_size(static_cast(git_blob_rawsize(blob))) + ")"; } break; } case GIT_OBJECT_TREE: { view.kind = ObjectKind::Tree; view.type_name = "tree"; git_tree *tree = reinterpret_cast(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(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(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; }