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 <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
delvh 2023-07-27 12:47:41 +02:00 committed by GitHub
parent bd7b5e61aa
commit bd6ef71854
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 202 additions and 42 deletions

View file

@ -166,6 +166,7 @@ func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
// NotFoundOrServerError use error check function to determine if the 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, // 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. // 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) { func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
if errCheck(logErr) { if errCheck(logErr) {
ctx.notFoundInternal(logMsg, logErr) ctx.notFoundInternal(logMsg, logErr)

View file

@ -20,7 +20,6 @@ import (
// Commit represents a git commit. // Commit represents a git commit.
type Commit struct { type Commit struct {
Branch string // Branch this commit belongs to
Tree Tree
ID SHA1 // The ID of this commit object ID SHA1 // The ID of this commit object
Author *Signature Author *Signature
@ -432,31 +431,6 @@ func (c *Commit) GetBranchName() (string, error) {
return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil 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. // CommitFileStatus represents status of files in a commit.
type CommitFileStatus struct { type CommitFileStatus struct {
Added []string Added []string

View file

@ -3,7 +3,61 @@
package git package git
import (
"context"
"strings"
"code.gitea.io/gitea/modules/util"
)
// GetRefs returns all references of the repository. // GetRefs returns all references of the repository.
func (repo *Repository) GetRefs() ([]*Reference, error) { func (repo *Repository) GetRefs() ([]*Reference, error) {
return repo.GetRefsFiltered("") 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
}

View file

@ -1170,6 +1170,9 @@ commit_graph.select = Select branches
commit_graph.hide_pr_refs = Hide Pull Requests commit_graph.hide_pr_refs = Hide Pull Requests
commit_graph.monochrome = Mono commit_graph.monochrome = Mono
commit_graph.color = Color 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 blame = Blame
download_file = Download file download_file = Download file
normal_view = Normal View normal_view = Normal View

View file

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
git_service "code.gitea.io/gitea/services/repository"
) )
const ( const (
@ -255,6 +256,15 @@ func FileHistory(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplCommits) 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 // Diff show different from current commit to previous commit
func Diff(ctx *context.Context) { func Diff(ctx *context.Context) {
ctx.Data["PageIsDiff"] = true ctx.Data["PageIsDiff"] = true
@ -374,11 +384,6 @@ func Diff(ctx *context.Context) {
return return
} }
ctx.Data["TagName"], err = commit.GetTagName()
if err != nil {
ctx.ServerError("commit.GetTagName", err)
return
}
ctx.HTML(http.StatusOK, tplCommitPage) ctx.HTML(http.StatusOK, tplCommitPage)
} }

View file

@ -1337,6 +1337,7 @@ func registerRoutes(m *web.Route) {
m.Group("", func() { m.Group("", func() {
m.Get("/graph", repo.Graph) 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})$}", 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) m.Get("/cherry-pick/{sha:([a-f0-9]{7,40})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)

View file

@ -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
}

View file

@ -0,0 +1,18 @@
<div class="branch-and-tag-area" data-text-default-branch-tooltip="{{.locale.Tr "repo.commit.contained_in_default_branch"}}">
<button class="ui button ellipsis-button load-branches-and-tags gt-mt-3" aria-expanded="false"
data-fetch-url="{{.RepoLink}}/commit/{{.CommitID}}/load-branches-and-tags"
data-tooltip-content="{{.locale.Tr "repo.commit.load_referencing_branches_and_tags"}}"
>...</button>
<div class="branch-and-tag-detail gt-hidden">
<div class="divider"></div>
<div>{{.locale.Tr "repo.commit.contained_in"}}</div>
<div class="gt-df gt-mt-3">
<div class="gt-p-2">{{svg "octicon-git-branch"}}</div>
<div class="branch-area flex-text-block gt-f1"></div>
</div>
<div class="gt-df gt-mt-3">
<div class="gt-p-2">{{svg "octicon-tag"}}</div>
<div class="tag-area flex-text-block gt-f1"></div>
</div>
</div>
</div>

View file

@ -137,12 +137,7 @@
{{if IsMultilineCommitMessage .Commit.Message}} {{if IsMultilineCommitMessage .Commit.Message}}
<pre class="commit-body gt-mt-0">{{RenderCommitBody $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre> <pre class="commit-body gt-mt-0">{{RenderCommitBody $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
{{end}} {{end}}
{{if .BranchName}} {{template "repo/commit_load_branches_and_tags" .}}
<span class="text grey gt-mr-3">{{svg "octicon-git-branch" 16 "gt-mr-2"}}{{.BranchName}}</span>
{{end}}
{{if .TagName}}
<span class="text grey gt-mr-3">{{svg "octicon-tag" 16 "gt-mr-2"}}{{.TagName}}</span>
{{end}}
</div> </div>
<div class="ui attached segment gt-df gt-ac gt-sb gt-py-2 commit-header-row gt-fw {{$class}}"> <div class="ui attached segment gt-df gt-ac gt-sb gt-py-2 commit-header-row gt-fw {{$class}}">
<div class="gt-df gt-ac author"> <div class="gt-df gt-ac author">

View file

@ -70,7 +70,7 @@
{{end}} {{end}}
</span> </span>
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<button class="ui button ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}} {{end}}
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses "root" $}} {{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses "root" $}}
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}

View file

@ -40,7 +40,7 @@
<span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $commitLink $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</span> <span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $commitLink $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<button class="ui button ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
{{end}} {{end}}
{{if IsMultilineCommitMessage .Message}} {{if IsMultilineCommitMessage .Message}}
<pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</pre>

View file

@ -28,7 +28,7 @@
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}} {{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span> <span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .LatestCommit.Message}} {{if IsMultilineCommitMessage .LatestCommit.Message}}
<button class="ui button ellipsis-button" aria-expanded="false">...</button> <button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre> <pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
{{end}} {{end}}
</span> </span>

View file

@ -5,7 +5,7 @@ import {toggleElem} from '../utils/dom.js';
const {csrfToken} = window.config; const {csrfToken} = window.config;
export function initRepoEllipsisButton() { export function initRepoEllipsisButton() {
$('.ellipsis-button').on('click', function (e) { $('.js-toggle-commit-body').on('click', function (e) {
e.preventDefault(); e.preventDefault();
const expanded = $(this).attr('aria-expanded') === 'true'; const expanded = $(this).attr('aria-expanded') === 'true';
toggleElem($(this).parent().find('.commit-body')); toggleElem($(this).parent().find('.commit-body'));

View file

@ -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));
}
}

View file

@ -459,7 +459,7 @@ async function onEditContent(event) {
} }
export function initRepository() { export function initRepository() {
if ($('.repository').length === 0) { if ($('.page-content.repository').length === 0) {
return; return;
} }

View file

@ -83,6 +83,7 @@ import {initGiteaFomantic} from './modules/fomantic.js';
import {onDomReady} from './utils/dom.js'; import {onDomReady} from './utils/dom.js';
import {initRepoIssueList} from './features/repo-issue-list.js'; import {initRepoIssueList} from './features/repo-issue-list.js';
import {initCommonIssueListQuickGoto} from './features/common-issue-list.js'; import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
// Init Gitea's Fomantic settings // Init Gitea's Fomantic settings
initGiteaFomantic(); initGiteaFomantic();
@ -141,6 +142,7 @@ onDomReady(() => {
initRepoCodeView(); initRepoCodeView();
initRepoCommentForm(); initRepoCommentForm();
initRepoEllipsisButton(); initRepoEllipsisButton();
initRepoDiffCommitBranchesAndTags();
initRepoCommitLastCommitLoader(); initRepoCommitLastCommitLoader();
initRepoEditor(); initRepoEditor();
initRepoGraphGit(); initRepoGraphGit();