/* MIT License Copyright (c) 2026 Poul Sander Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "git_repo.h" #include "archive.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); } // 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(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; } void archive_tree(git_repository *repo, git_tree *tree, const std::string &archive_prefix, const std::string &repo_prefix, std::time_t mtime, TarGzWriter &tar); // Resolves a submodule to its repository, reads the recorded commit tree and // archives it under `archive_path`. Throws if the submodule is not registered, // not initialized, or missing the recorded commit, so a partial archive is // never produced. void archive_submodule(git_repository *parent, const std::string &repo_path, const std::string &archive_path, const git_oid *sub_commit, std::time_t mtime, TarGzWriter &tar) { git_submodule *sm = nullptr; if (git_submodule_lookup(&sm, parent, repo_path.c_str()) != 0) throw GitError("submodule not registered in .gitmodules: " + repo_path); git_repository *sub_repo = nullptr; const int open_err = git_submodule_open(&sub_repo, sm); git_submodule_free(sm); if (open_err != 0) throw GitError("submodule not initialized (cannot open): " + repo_path); git_commit *commit = nullptr; const int commit_err = git_commit_lookup(&commit, sub_repo, sub_commit); if (commit_err != 0) { git_repository_free(sub_repo); throw GitError("submodule commit " + oid_to_string(sub_commit) + " not present: " + repo_path); } git_tree *tree = nullptr; const int tree_err = git_commit_tree(&tree, commit); git_commit_free(commit); if (tree_err != 0) { git_repository_free(sub_repo); check(tree_err, "read submodule tree"); } tar.add_dir(archive_path, mtime); archive_tree(sub_repo, tree, archive_path, "", mtime, tar); git_tree_free(tree); git_repository_free(sub_repo); } // Recursively writes the entries of `tree` into the archive. `archive_prefix` // is the path shown inside the tarball; `repo_prefix` is the path relative to // `repo` (used for submodule lookup) and resets when descending into a submodule. void archive_tree(git_repository *repo, git_tree *tree, const std::string &archive_prefix, const std::string &repo_prefix, std::time_t mtime, TarGzWriter &tar) { const size_t count = git_tree_entrycount(tree); for (size_t i = 0; i < count; ++i) { const git_tree_entry *entry = git_tree_entry_byindex(tree, i); const std::string name = git_tree_entry_name(entry); const std::string archive_path = archive_prefix + "/" + name; const std::string repo_path = repo_prefix.empty() ? name : repo_prefix + "/" + name; const git_oid *id = git_tree_entry_id(entry); switch (git_tree_entry_type(entry)) { case GIT_OBJECT_TREE: { git_tree *child = nullptr; check(git_tree_lookup(&child, repo, id), "lookup tree"); tar.add_dir(archive_path, mtime); archive_tree(repo, child, archive_path, repo_path, mtime, tar); git_tree_free(child); break; } case GIT_OBJECT_BLOB: { git_blob *blob = nullptr; check(git_blob_lookup(&blob, repo, id), "lookup blob"); const char *data = static_cast(git_blob_rawcontent(blob)); const auto size = static_cast(git_blob_rawsize(blob)); if (git_tree_entry_filemode(entry) == GIT_FILEMODE_LINK) tar.add_symlink(archive_path, std::string(data, size), mtime); else tar.add_file(archive_path, git_tree_entry_filemode(entry), mtime, data, size); git_blob_free(blob); break; } case GIT_OBJECT_COMMIT: // gitlink: a submodule archive_submodule(repo, repo_path, archive_path, id, mtime, tar); break; default: break; } } } } // 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::remote_url() const { git_remote *remote = nullptr; if (git_remote_lookup(&remote, repo_, "origin") != 0) return ""; const char *url = git_remote_url(remote); const std::string result = url ? url : ""; git_remote_free(remote); 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); e.is_image = !e.is_dir && !e.is_submodule && util::image_mime_type(e.name).length() > 0; 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 char *body = git_commit_body(commit); info.body = body ? body : ""; 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"); // Newest first; fall back to name for tags sharing a timestamp. std::sort(result.begin(), result.end(), [](const TagInfo &a, const TagInfo &b) { if (a.time != b.time) return a.time > b.time; return a.name < b.name; }); return result; } std::vector GitRepo::tags_for_commit(const std::string &commit_oid) const { std::vector 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(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; } std::vector GitRepo::branches() const { const std::string def = default_branch(); std::vector 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(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 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 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 *>(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; } void GitRepo::write_archive(const std::string &commit_oid, const std::string &top_dir, const std::string &out_path) const { git_commit *commit = lookup_commit_by_oid(repo_, commit_oid); std::time_t mtime = 0; if (const git_signature *committer = git_commit_committer(commit)) mtime = committer->when.time; git_tree *tree = nullptr; const int err = git_commit_tree(&tree, commit); git_commit_free(commit); check(err, "read commit tree"); try { TarGzWriter tar(out_path); tar.add_dir(top_dir, mtime); archive_tree(repo_, tree, top_dir, "", mtime, tar); tar.finish(); } catch (...) { git_tree_free(tree); throw; } git_tree_free(tree); }