From bd6ef718548767ff209048eb8443a067106908bf Mon Sep 17 00:00:00 2001 From: delvh Date: Thu, 27 Jul 2023 12:47:41 +0200 Subject: [PATCH] Show branches and tags that contain a commit (#25180) Now, you can see for a commit which existing branches and tags contain it. You first have to click on the `load branches and tags` button, they are not preloaded by default. All branches and tags are ordered descending by creation date. You can even see without much hassle if the given commit is already part of the default branch. Closes #25152 ## Screenshots ### Initial ![image](https://github.com/go-gitea/gitea/assets/51889757/84db2c0b-aaef-4f69-ab92-0b812793d2ad) ### Loaded ![image](https://github.com/go-gitea/gitea/assets/51889757/a9b84e66-8e44-4c55-b017-c37f4a45f41b) --------- Co-authored-by: silverwind Co-authored-by: wxiaoguang --- modules/context/context_response.go | 1 + modules/git/commit.go | 26 --------- modules/git/repo_ref.go | 54 ++++++++++++++++++ options/locale/locale_en-US.ini | 3 + routers/web/repo/commit.go | 15 +++-- routers/web/web.go | 1 + services/repository/commit.go | 55 +++++++++++++++++++ .../repo/commit_load_branches_and_tags.tmpl | 18 ++++++ templates/repo/commit_page.tmpl | 7 +-- templates/repo/commits_list.tmpl | 2 +- templates/repo/commits_list_small.tmpl | 2 +- templates/repo/view_list.tmpl | 2 +- web_src/js/features/repo-commit.js | 2 +- web_src/js/features/repo-diff-commit.js | 52 ++++++++++++++++++ web_src/js/features/repo-legacy.js | 2 +- web_src/js/index.js | 2 + 16 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 services/repository/commit.go create mode 100644 templates/repo/commit_load_branches_and_tags.tmpl create mode 100644 web_src/js/features/repo-diff-commit.js diff --git a/modules/context/context_response.go b/modules/context/context_response.go index bb3ccf69ce42..9dc6d1fc0ec5 100644 --- a/modules/context/context_response.go +++ b/modules/context/context_response.go @@ -166,6 +166,7 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) { // NotFoundOrServerError use error check function to determine if the error // is about not found. It responds with 404 status code for not found error, // or error context description for logging purpose of 500 server error. +// TODO: remove the "errCheck" and use util.ErrNotFound to check func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) { if errCheck(logErr) { ctx.notFoundInternal(logMsg, logErr) diff --git a/modules/git/commit.go b/modules/git/commit.go index 729e3b4672a7..c44882d88617 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -20,7 +20,6 @@ import ( // Commit represents a git commit. type Commit struct { - Branch string // Branch this commit belongs to Tree ID SHA1 // The ID of this commit object Author *Signature @@ -432,31 +431,6 @@ func (c *Commit) GetBranchName() (string, error) { return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil } -// LoadBranchName load branch name for commit -func (c *Commit) LoadBranchName() (err error) { - if len(c.Branch) != 0 { - return nil - } - - c.Branch, err = c.GetBranchName() - return err -} - -// GetTagName gets the current tag name for given commit -func (c *Commit) GetTagName() (string, error) { - data, _, err := NewCommand(c.repo.Ctx, "describe", "--exact-match", "--tags", "--always").AddDynamicArguments(c.ID.String()).RunStdString(&RunOpts{Dir: c.repo.Path}) - if err != nil { - // handle special case where there is no tag for this commit - if strings.Contains(err.Error(), "no tag exactly matches") { - return "", nil - } - - return "", err - } - - return strings.TrimSpace(data), nil -} - // CommitFileStatus represents status of files in a commit. type CommitFileStatus struct { Added []string diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 54e424bb832a..8eaa17cb0418 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -3,7 +3,61 @@ package git +import ( + "context" + "strings" + + "code.gitea.io/gitea/modules/util" +) + // GetRefs returns all references of the repository. func (repo *Repository) GetRefs() ([]*Reference, error) { return repo.GetRefsFiltered("") } + +// ListOccurrences lists all refs of the given refType the given commit appears in sorted by creation date DESC +// refType should only be a literal "branch" or "tag" and nothing else +func (repo *Repository) ListOccurrences(ctx context.Context, refType, commitSHA string) ([]string, error) { + cmd := NewCommand(ctx) + if refType == "branch" { + cmd.AddArguments("branch") + } else if refType == "tag" { + cmd.AddArguments("tag") + } else { + return nil, util.NewInvalidArgumentErrorf(`can only use "branch" or "tag" for refType, but got %q`, refType) + } + stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains").AddDynamicArguments(commitSHA).RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + + refs := strings.Split(strings.TrimSpace(stdout), "\n") + if refType == "branch" { + return parseBranches(refs), nil + } + return parseTags(refs), nil +} + +func parseBranches(refs []string) []string { + results := make([]string, 0, len(refs)) + for _, ref := range refs { + if strings.HasPrefix(ref, "* ") { // current branch (main branch) + results = append(results, ref[len("* "):]) + } else if strings.HasPrefix(ref, " ") { // all other branches + results = append(results, ref[len(" "):]) + } else if ref != "" { + results = append(results, ref) + } + } + return results +} + +func parseTags(refs []string) []string { + results := make([]string, 0, len(refs)) + for _, ref := range refs { + if ref != "" { + results = append(results, ref) + } + } + return results +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 25fb15543576..dc88c422b5ea 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1170,6 +1170,9 @@ commit_graph.select = Select branches commit_graph.hide_pr_refs = Hide Pull Requests commit_graph.monochrome = Mono commit_graph.color = Color +commit.contained_in = This commit is contained in: +commit.contained_in_default_branch = This commit is part of the default branch +commit.load_referencing_branches_and_tags = Load branches and tags referencing this commit blame = Blame download_file = Download file normal_view = Normal View diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index e88f1139f8b7..5b32591b8914 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/gitdiff" + git_service "code.gitea.io/gitea/services/repository" ) const ( @@ -255,6 +256,15 @@ func FileHistory(ctx *context.Context) { ctx.HTML(http.StatusOK, tplCommits) } +func LoadBranchesAndTags(ctx *context.Context) { + response, err := git_service.LoadBranchesAndTags(ctx, ctx.Repo, ctx.Params("sha")) + if err == nil { + ctx.JSON(http.StatusOK, response) + return + } + ctx.NotFoundOrServerError(fmt.Sprintf("could not load branches and tags the commit %s belongs to", ctx.Params("sha")), git.IsErrNotExist, err) +} + // Diff show different from current commit to previous commit func Diff(ctx *context.Context) { ctx.Data["PageIsDiff"] = true @@ -374,11 +384,6 @@ func Diff(ctx *context.Context) { return } - ctx.Data["TagName"], err = commit.GetTagName() - if err != nil { - ctx.ServerError("commit.GetTagName", err) - return - } ctx.HTML(http.StatusOK, tplCommitPage) } diff --git a/routers/web/web.go b/routers/web/web.go index d7ef2fb82f22..0b519614453a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1337,6 +1337,7 @@ func registerRoutes(m *web.Route) { m.Group("", func() { m.Get("/graph", repo.Graph) m.Get("/commit/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) + m.Get("/commit/{sha:([a-f0-9]{7,40})$}/load-branches-and-tags", repo.LoadBranchesAndTags) m.Get("/cherry-pick/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.CherryPick) }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) diff --git a/services/repository/commit.go b/services/repository/commit.go new file mode 100644 index 000000000000..2497910a838d --- /dev/null +++ b/services/repository/commit.go @@ -0,0 +1,55 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "fmt" + + gitea_ctx "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/util" +) + +type ContainedLinks struct { // TODO: better name? + Branches []*namedLink `json:"branches"` + Tags []*namedLink `json:"tags"` + DefaultBranch string `json:"default_branch"` +} + +type namedLink struct { // TODO: better name? + Name string `json:"name"` + WebLink string `json:"web_link"` +} + +// LoadBranchesAndTags creates a new repository branch +func LoadBranchesAndTags(ctx context.Context, baseRepo *gitea_ctx.Repository, commitSHA string) (*ContainedLinks, error) { + containedTags, err := baseRepo.GitRepo.ListOccurrences(ctx, "tag", commitSHA) + if err != nil { + return nil, fmt.Errorf("encountered a problem while querying %s: %w", "tags", err) + } + containedBranches, err := baseRepo.GitRepo.ListOccurrences(ctx, "branch", commitSHA) + if err != nil { + return nil, fmt.Errorf("encountered a problem while querying %s: %w", "branches", err) + } + + result := &ContainedLinks{ + DefaultBranch: baseRepo.Repository.DefaultBranch, + Branches: make([]*namedLink, 0, len(containedBranches)), + Tags: make([]*namedLink, 0, len(containedTags)), + } + for _, tag := range containedTags { + // TODO: Use a common method to get the link to a branch/tag instead of hard-coding it here + result.Tags = append(result.Tags, &namedLink{ + Name: tag, + WebLink: fmt.Sprintf("%s/src/tag/%s", baseRepo.RepoLink, util.PathEscapeSegments(tag)), + }) + } + for _, branch := range containedBranches { + result.Branches = append(result.Branches, &namedLink{ + Name: branch, + WebLink: fmt.Sprintf("%s/src/branch/%s", baseRepo.RepoLink, util.PathEscapeSegments(branch)), + }) + } + return result, nil +} diff --git a/templates/repo/commit_load_branches_and_tags.tmpl b/templates/repo/commit_load_branches_and_tags.tmpl new file mode 100644 index 000000000000..c19aa55c5619 --- /dev/null +++ b/templates/repo/commit_load_branches_and_tags.tmpl @@ -0,0 +1,18 @@ +
+ +
+
+
{{.locale.Tr "repo.commit.contained_in"}}
+
+
{{svg "octicon-git-branch"}}
+
+
+
+
{{svg "octicon-tag"}}
+
+
+
+
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index e4aad30fa7cf..bd33a304435a 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -137,12 +137,7 @@ {{if IsMultilineCommitMessage .Commit.Message}}
{{RenderCommitBody $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}
{{end}} - {{if .BranchName}} - {{svg "octicon-git-branch" 16 "gt-mr-2"}}{{.BranchName}} - {{end}} - {{if .TagName}} - {{svg "octicon-tag" 16 "gt-mr-2"}}{{.TagName}} - {{end}} + {{template "repo/commit_load_branches_and_tags" .}}
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl index 6768bcb5137f..ef9d0654566f 100644 --- a/templates/repo/commits_list.tmpl +++ b/templates/repo/commits_list.tmpl @@ -70,7 +70,7 @@ {{end}} {{if IsMultilineCommitMessage .Message}} - + {{end}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses "root" $}} {{if IsMultilineCommitMessage .Message}} diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl index 6bbc19529f94..57c9fd17ef77 100644 --- a/templates/repo/commits_list_small.tmpl +++ b/templates/repo/commits_list_small.tmpl @@ -40,7 +40,7 @@ {{RenderCommitMessageLinkSubject $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $commitLink $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}} {{if IsMultilineCommitMessage .Message}} - + {{end}} {{if IsMultilineCommitMessage .Message}}
{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 13b4d3d3d322..3eabf9f181e5 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -28,7 +28,7 @@ {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} {{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}} {{if IsMultilineCommitMessage .LatestCommit.Message}} - +
{{RenderCommitBody $.Context .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}
{{end}}
diff --git a/web_src/js/features/repo-commit.js b/web_src/js/features/repo-commit.js index 7e4e40806b6c..7240bf398aec 100644 --- a/web_src/js/features/repo-commit.js +++ b/web_src/js/features/repo-commit.js @@ -5,7 +5,7 @@ import {toggleElem} from '../utils/dom.js'; const {csrfToken} = window.config; export function initRepoEllipsisButton() { - $('.ellipsis-button').on('click', function (e) { + $('.js-toggle-commit-body').on('click', function (e) { e.preventDefault(); const expanded = $(this).attr('aria-expanded') === 'true'; toggleElem($(this).parent().find('.commit-body')); diff --git a/web_src/js/features/repo-diff-commit.js b/web_src/js/features/repo-diff-commit.js new file mode 100644 index 000000000000..968f318e63d5 --- /dev/null +++ b/web_src/js/features/repo-diff-commit.js @@ -0,0 +1,52 @@ +import {hideElem, showElem, toggleElem} from '../utils/dom.js'; + +async function loadBranchesAndTags(area, loadingButton) { + loadingButton.classList.add('disabled'); + try { + const res = await fetch(loadingButton.getAttribute('data-fetch-url')); + const data = await res.json(); + hideElem(loadingButton); + addTags(area, data.tags); + addBranches(area, data.branches, data.default_branch); + showElem(area.querySelectorAll('.branch-and-tag-detail')); + } finally { + loadingButton.classList.remove('disabled'); + } +} + +function addTags(area, tags) { + const tagArea = area.querySelector('.tag-area'); + toggleElem(tagArea, tags.length > 0); + for (const tag of tags) { + addLink(tagArea, tag.web_link, tag.name); + } +} + +function addBranches(area, branches, defaultBranch) { + const defaultBranchTooltip = area.getAttribute('data-text-default-branch-tooltip'); + const branchArea = area.querySelector('.branch-area'); + toggleElem(branchArea, branches.length > 0); + for (const branch of branches) { + const tooltip = defaultBranch === branch.name ? defaultBranchTooltip : null; + addLink(branchArea, branch.web_link, branch.name, tooltip); + } +} + +function addLink(parent, href, text, tooltip) { + const link = document.createElement('a'); + link.classList.add('muted', 'gt-px-2'); + link.href = href; + link.textContent = text; + if (tooltip) { + link.classList.add('gt-border-secondary', 'gt-rounded'); + link.setAttribute('data-tooltip-content', tooltip); + } + parent.append(link); +} + +export function initRepoDiffCommitBranchesAndTags() { + for (const area of document.querySelectorAll('.branch-and-tag-area')) { + const btn = area.querySelector('.load-branches-and-tags'); + btn.addEventListener('click', () => loadBranchesAndTags(area, btn)); + } +} diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index f23ff45470c1..5991df6322c5 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -459,7 +459,7 @@ async function onEditContent(event) { } export function initRepository() { - if ($('.repository').length === 0) { + if ($('.page-content.repository').length === 0) { return; } diff --git a/web_src/js/index.js b/web_src/js/index.js index 0c786f96fbef..8bd219bbe157 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -83,6 +83,7 @@ import {initGiteaFomantic} from './modules/fomantic.js'; import {onDomReady} from './utils/dom.js'; import {initRepoIssueList} from './features/repo-issue-list.js'; import {initCommonIssueListQuickGoto} from './features/common-issue-list.js'; +import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js'; // Init Gitea's Fomantic settings initGiteaFomantic(); @@ -141,6 +142,7 @@ onDomReady(() => { initRepoCodeView(); initRepoCommentForm(); initRepoEllipsisButton(); + initRepoDiffCommitBranchesAndTags(); initRepoCommitLastCommitLoader(); initRepoEditor(); initRepoGraphGit();