git repos / sago_web_git

src/git_repo.h

raw · blame · history

/*
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.

*/

#pragma once

#include <cstdint>
#include <ctime>
#include <stdexcept>
#include <string>
#include <vector>

struct git_repository;

// Thrown for any libgit2 failure or invalid lookup. Handlers translate this
// into an HTTP error response.
class GitError : public std::runtime_error {
public:
    explicit GitError(const std::string &what) : std::runtime_error(what) {}
};

struct TreeEntry {
    std::string name;     // last path component
    std::string path;     // full path relative to repo root
    std::string oid;      // hex object id
    bool is_dir = false;  // tree
    bool is_submodule = false;
    uint64_t size = 0;    // blob size in bytes (0 for non-blobs)
    bool is_image = false; // blob whose name has an image extension
};

struct CommitInfo {
    std::string oid;
    std::string short_oid;
    std::string summary;
    std::string body; // message past the summary, "" when there is none
    std::string author_name;
    std::string author_email;
    std::time_t time = 0;
};

struct BlameLine {
    int number = 0;        // 1-based line number
    std::string short_oid; // commit that last touched the line
    std::string author;
    std::time_t time = 0;
    std::string content;   // the line text (without newline)
};

struct TagInfo {
    std::string name;
    std::string target_oid; // commit the tag resolves to
    std::string short_oid;
    std::time_t time = 0;
    bool annotated = false;
};

struct BranchInfo {
    std::string name;
    std::string target_oid; // commit the branch points at
    std::string short_oid;
    std::time_t time = 0;
    bool is_default = false; // true for the repository's HEAD branch
};

struct TagDetail {
    std::string name;
    std::string target_oid;
    std::string short_oid;
    bool annotated = false;
    std::string tagger_name;
    std::time_t time = 0;
    std::string message;
};

struct FileChange {
    char status = '?'; // A, M, D, R, C, T
    std::string path;
    std::string old_path; // set for renames/copies
};

struct DiffLine {
    std::string css;     // CSS class: d-add, d-del, d-hunk, d-file, d-ctx
    std::string content; // line text (with +/- prefix for code lines)
};

struct CommitDetail {
    std::string oid;
    std::string short_oid;
    std::string author_name;
    std::string author_email;
    std::time_t time = 0;
    std::string message;
    std::vector<std::string> parents;
    std::string first_parent; // "" for a root commit; used for "before" links
    std::vector<FileChange> files;
    std::vector<DiffLine> diff;
};

enum class ObjectKind { Blob, Tree, Commit, Tag, Unknown };

struct ObjectView {
    ObjectKind kind = ObjectKind::Unknown;
    std::string oid;
    std::string type_name;
    std::string body;             // textual summary or blob preview
    bool is_binary = false;
    std::vector<TreeEntry> entries; // populated for trees
};

// libgit2 process-wide init/shutdown. Construct exactly once in main().
class GitLibrary {
public:
    GitLibrary();
    ~GitLibrary();
    GitLibrary(const GitLibrary &) = delete;
    GitLibrary &operator=(const GitLibrary &) = delete;
};

// Wraps a single opened repository. Not thread safe: open one per request.
class GitRepo {
public:
    explicit GitRepo(const std::string &path);
    ~GitRepo();
    GitRepo(const GitRepo &) = delete;
    GitRepo &operator=(const GitRepo &) = delete;

    // Short name of the default branch (HEAD), e.g. "main".
    std::string default_branch() const;

    // URL of the "origin" remote, or "" when there is no such remote.
    std::string remote_url() const;

    // Resolve a revision to a full commit oid. Empty id means HEAD.
    std::string resolve_commit(const std::string &id) const;

    // Directory listing of `path` ("" = root) at the given commit.
    std::vector<TreeEntry> list_tree(const std::string &commit_oid,
                                     const std::string &path) const;

    // Read a blob located at `path` within the commit tree.
    // Returns false if no such path exists.
    bool read_file(const std::string &commit_oid, const std::string &path,
                   std::string &out, bool &is_binary) const;

    // First README* file at the root of the commit tree, or "" if none.
    std::string find_readme(const std::string &commit_oid) const;

    std::vector<BlameLine> blame(const std::string &commit_oid,
                                 const std::string &path) const;

    std::vector<CommitInfo> log(const std::string &start_oid, int limit) const;

    // All tags, sorted by name.
    std::vector<TagInfo> tags() const;

    // Names of tags whose target commit is `commit_oid`, sorted by name.
    std::vector<std::string> tags_for_commit(const std::string &commit_oid) const;

    // Details of a single tag by short name (e.g. "v1.0").
    TagDetail tag_detail(const std::string &name) const;

    // All local branches, sorted by name.
    std::vector<BranchInfo> branches() const;

    // Local branches whose history contains `commit_oid`, sorted by name.
    std::vector<BranchInfo> branches_for_commit(const std::string &commit_oid) const;

    // Commit metadata plus the diff against its first parent.
    CommitDetail commit_detail(const std::string &oid) const;

    // The commit's diff as a raw unified patch.
    std::string commit_patch(const std::string &oid) const;

    // Inspect an arbitrary object by (possibly abbreviated) oid.
    ObjectView inspect(const std::string &oid) const;

    // Writes a gzip-compressed tar archive of the commit's tree to `out_path`,
    // recursing into initialized submodules. `top_dir` is the directory placed
    // at the archive root (e.g. "repo_v1.0"). Throws GitError when a submodule
    // cannot be resolved or any other libgit2 lookup fails; the archive writer
    // throws ArchiveError on I/O failure.
    void write_archive(const std::string &commit_oid, const std::string &top_dir,
                       const std::string &out_path) const;

private:
    git_repository *repo_ = nullptr;
};