diff --git a/src/application.cpp b/src/application.cpp index 8e39241..87478b2 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -46,6 +46,7 @@ GitApp::GitApp(cppcms::service &srv, const RepoScanner *scanner) dispatcher().assign("^/repos/([^/]+)/commit/([0-9a-f]+)$", &GitApp::commit, this, 1, 2); dispatcher().assign("^/repos/([^/]+)/tags/?$", &GitApp::tags, this, 1); dispatcher().assign("^/repos/([^/]+)/tags/(.+)$", &GitApp::tag, this, 1, 2); + dispatcher().assign("^/repos/([^/]+)/branches/?$", &GitApp::branches, this, 1); dispatcher().assign("^/repos/([^/]+)/?$", &GitApp::repo_home, this, 1); } @@ -292,6 +293,21 @@ void GitApp::tag(std::string name, std::string tag_name) { } } +void GitApp::branches(std::string name) { + std::unique_ptr repo = open(name); + if (!repo) + return; + try { + content::branches_view c; + c.repo = name; + c.title = "branches " + name; + c.branches = repo->branches(); + render(SKIN, "branches_view", c); + } catch (const GitError &e) { + show_error(404, e.what()); + } +} + void GitApp::commit(std::string name, std::string oid) { std::unique_ptr repo = open(name); if (!repo) @@ -301,6 +317,13 @@ void GitApp::commit(std::string name, std::string oid) { c.repo = name; c.title = "commit " + oid; c.commit = repo->commit_detail(oid); + c.branches = repo->branches_for_commit(c.commit.oid); + for (const BranchInfo &b : c.branches) { + if (b.is_default) { + c.on_default_branch = true; + break; + } + } render(SKIN, "commit_view", c); } catch (const GitError &e) { show_error(404, e.what()); diff --git a/src/application.h b/src/application.h index 497f873..2b66f1d 100644 --- a/src/application.h +++ b/src/application.h @@ -29,6 +29,7 @@ private: void object(std::string name, std::string oid); void tags(std::string name); void tag(std::string name, std::string tag_name); + void branches(std::string name); void commit(std::string name, std::string oid); void commit_patch(std::string name, std::string oid); void static_file(std::string rel); diff --git a/src/content.h b/src/content.h index 0fbdcf2..de68461 100644 --- a/src/content.h +++ b/src/content.h @@ -78,8 +78,14 @@ struct tag_view : public master { TagDetail tag; }; +struct branches_view : public master { + std::vector branches; +}; + struct commit_view : public master { CommitDetail commit; + std::vector branches; // local branches whose history contains the commit + bool on_default_branch = false; // when true, branch annotation is suppressed }; } // namespace content diff --git a/src/git_repo.cpp b/src/git_repo.cpp index 4230eeb..d4fca83 100644 --- a/src/git_repo.cpp +++ b/src/git_repo.cpp @@ -47,6 +47,14 @@ git_commit *lookup_commit(git_repository *repo, const std::string &id) { 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"); @@ -497,6 +505,57 @@ TagDetail GitRepo::tag_detail(const std::string &name) const { 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); diff --git a/src/git_repo.h b/src/git_repo.h index 42e30be..0288d86 100644 --- a/src/git_repo.h +++ b/src/git_repo.h @@ -49,6 +49,14 @@ struct TagInfo { 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; @@ -143,6 +151,12 @@ public: // 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 branches() const; + + // Local branches whose history contains `commit_oid`, sorted by name. + std::vector 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; diff --git a/templates/skin.tmpl b/templates/skin.tmpl index 6ecc5e5..b590bfa 100644 --- a/templates/skin.tmpl +++ b/templates/skin.tmpl @@ -22,6 +22,7 @@ <% end %> @@ -229,14 +230,41 @@ points to <%= tag.shor <% end template %> <% end view %> +<% view branches_view uses content::branches_view extends master %> +<% template content_body() %> +

branches: <%= repo %>

+ +<% foreach b in branches %><% item %> + + + + + + + +<% end item %><% empty %> + +<% end %> +
<%= b.short_oid %><%= b.name %><% if b.is_default %> (default)<% end %>logbrowse files<% c++ out() << util::format_time(b.time); %>
no branches
+<% end template %> +<% end view %> + <% view commit_view uses content::commit_view extends master %> <% template content_body() %>

commit <%= commit.short_oid %>

+<% if (not content.on_default_branch) %> +<% if (content.branches.size() > 0) %> +

branches: <% foreach b in branches %><% item %><%= b.name %> <% end item %><% end foreach %>

+<% else %> +

not on any branch

+<% end %> +<% end %>

<%= commit.author_name %><% if (content.commit.author_email.length() > 0) %> <<%= commit.author_email %>><% end %> · <% c++ out() << util::format_time(content.commit.time); %>
<%= commit.oid %> patchbrowse files <% foreach p in commit.parents %><% item %>
parent <%= p %><% end item %><% end foreach %>

<%= commit.message %>