return cancel
}
- tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
- if err != nil {
- ctx.ServerError("GetTagNamesByRepoID", err)
- return cancel
- }
- ctx.Data["Tags"] = tags
-
branchOpts := git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
IsDeletedBranch: util.OptionalBoolFalse,
return cancel
}
- // non empty repo should have at least 1 branch, so this repository's branches haven't been synced yet
+ // non-empty repo should have at least 1 branch, so this repository's branches haven't been synced yet
if branchesTotal == 0 { // fallback to do a sync immediately
branchesTotal, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
if err != nil {
}
}
- // FIXME: use paganation and async loading
- branchOpts.ExcludeBranchNames = []string{ctx.Repo.Repository.DefaultBranch}
- brs, err := git_model.FindBranchNames(ctx, branchOpts)
- if err != nil {
- ctx.ServerError("GetBranches", err)
- return cancel
- }
- // always put default branch on the top
- ctx.Data["Branches"] = append(branchOpts.ExcludeBranchNames, brs...)
ctx.Data["BranchesCount"] = branchesTotal
- // If not branch selected, try default one.
- // If default branch doesn't exist, fall back to some other branch.
+ // If no branch is set in the request URL, try to guess a default one.
if len(ctx.Repo.BranchName) == 0 {
if len(ctx.Repo.Repository.DefaultBranch) > 0 && gitRepo.IsBranchExist(ctx.Repo.Repository.DefaultBranch) {
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
- } else if len(brs) > 0 {
- ctx.Repo.BranchName = brs[0]
+ } else {
+ ctx.Repo.BranchName, _ = gitRepo.GetDefaultBranch()
+ if ctx.Repo.BranchName == "" {
+ // If it still can't get a default branch, fall back to default branch from setting.
+ // Something might be wrong. Either site admin should fix the repo sync or Gitea should fix a potential bug.
+ ctx.Repo.BranchName = setting.Repository.DefaultBranch
+ }
}
ctx.Repo.RefName = ctx.Repo.BranchName
}
}
ctx.Data["HeadBranches"] = headBranches
+ // For compare repo branches
+ PrepareBranchList(ctx)
+ if ctx.Written() {
+ return
+ }
+
headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
return nil
}
- brs, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
- RepoID: ctx.Repo.Repository.ID,
- ListOptions: db.ListOptions{
- ListAll: true,
- },
- IsDeletedBranch: util.OptionalBoolFalse,
- })
- if err != nil {
- ctx.ServerError("GetBranches", err)
+ PrepareBranchList(ctx)
+ if ctx.Written() {
return nil
}
- ctx.Data["Branches"] = brs
// Contains true if the user can create issue dependencies
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.Doer, isPull)
RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
+ tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
+ if err != nil {
+ ctx.ServerError("GetTagNamesByRepoID", err)
+ return
+ }
+ ctx.Data["Tags"] = tags
+
_, templateErrs := issue_service.GetTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
if errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates); len(errs) > 0 {
for k, v := range errs {
ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
}
+ // For sidebar
+ PrepareBranchList(ctx)
+
+ if ctx.Written() {
+ return
+ }
+
+ tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
+ if err != nil {
+ ctx.ServerError("GetTagNamesByRepoID", err)
+ return
+ }
+ ctx.Data["Tags"] = tags
ctx.HTML(http.StatusOK, tplIssueView)
}
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
+ // For PR commits page
+ PrepareBranchList(ctx)
+ if ctx.Written() {
+ return
+ }
getBranchData(ctx, issue)
ctx.HTML(http.StatusOK, tplPullCommits)
}
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
+ // For files changed page
+ PrepareBranchList(ctx)
+ if ctx.Written() {
+ return
+ }
upload.AddUploadContext(ctx, "comment")
ctx.HTML(http.StatusOK, tplPullFiles)
ctx.Data["Assignees"] = MakeSelfOnTop(ctx, assigneeUsers)
upload.AddUploadContext(ctx, "release")
+
+ // For New Release page
+ PrepareBranchList(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
+ if err != nil {
+ ctx.ServerError("GetTagNamesByRepoID", err)
+ return
+ }
+ ctx.Data["Tags"] = tags
+
ctx.HTML(http.StatusOK, tplReleaseNew)
}
ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true
+ tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
+ if err != nil {
+ ctx.ServerError("GetTagNamesByRepoID", err)
+ return
+ }
+ ctx.Data["Tags"] = tags
+
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplReleaseNew)
return
Data: results,
})
}
+
+type branchTagSearchResponse struct {
+ Results []string `json:"results"`
+}
+
+// GetBranchesList get branches for current repo'
+func GetBranchesList(ctx *context.Context) {
+ branchOpts := git_model.FindBranchOptions{
+ RepoID: ctx.Repo.Repository.ID,
+ IsDeletedBranch: util.OptionalBoolFalse,
+ ListOptions: db.ListOptions{
+ ListAll: true,
+ },
+ }
+ branches, err := git_model.FindBranchNames(ctx, branchOpts)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, err)
+ return
+ }
+ resp := &branchTagSearchResponse{}
+ // always put default branch on the top if it exists
+ if util.SliceContains(branches, ctx.Repo.Repository.DefaultBranch) {
+ branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
+ branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
+ }
+ resp.Results = branches
+ ctx.JSON(http.StatusOK, resp)
+}
+
+// GetTagList get tag list for current repo
+func GetTagList(ctx *context.Context) {
+ tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
+ if err != nil {
+ ctx.JSON(http.StatusInternalServerError, err)
+ return
+ }
+ resp := &branchTagSearchResponse{}
+ resp.Results = tags
+ ctx.JSON(http.StatusOK, resp)
+}
+
+func PrepareBranchList(ctx *context.Context) {
+ branchOpts := git_model.FindBranchOptions{
+ RepoID: ctx.Repo.Repository.ID,
+ IsDeletedBranch: util.OptionalBoolFalse,
+ ListOptions: db.ListOptions{
+ ListAll: true,
+ },
+ }
+ brs, err := git_model.FindBranchNames(ctx, branchOpts)
+ if err != nil {
+ ctx.ServerError("GetBranches", err)
+ return
+ }
+ // always put default branch on the top if it exists
+ if util.SliceContains(brs, ctx.Repo.Repository.DefaultBranch) {
+ brs = util.SliceRemoveAll(brs, ctx.Repo.Repository.DefaultBranch)
+ brs = append([]string{ctx.Repo.Repository.DefaultBranch}, brs...)
+ }
+ ctx.Data["Branches"] = brs
+}
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/forms"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/services/repository"
}
ctx.Data["ProtectedBranches"] = rules
+ repo.PrepareBranchList(ctx)
+ if ctx.Written() {
+ return
+ }
+
ctx.HTML(http.StatusOK, tplBranches)
}
ctx.Data["Title"] = ctx.Tr("repo.settings.branches.update_default_branch")
ctx.Data["PageIsSettingsBranches"] = true
+ repo.PrepareBranchList(ctx)
+ if ctx.Written() {
+ return
+ }
+
repo := ctx.Repo.Repository
switch ctx.FormString("action") {
}, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived())
m.Group("/branches", func() {
+ m.Get("/list", repo.GetBranchesList)
m.Group("/_new", func() {
m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
m.Group("/{username}/{reponame}", func() {
m.Group("/tags", func() {
m.Get("", repo.TagsList)
+ m.Get("/list", repo.GetTagList)
m.Get(".rss", feedEnabled, repo.TagsListFeedRSS)
m.Get(".atom", feedEnabled, repo.TagsListFeedAtom)
}, ctxDataSet("EnableFeed", setting.Other.EnableFeed),
'tagName': {{.root.TagName}},
'branchName': {{.root.BranchName}},
'noTag': {{.noTag}},
- 'branches': {{.root.Branches}},
- 'tags': {{.root.Tags}},
'defaultBranch': {{$defaultBranch}},
'enableFeed': {{.root.EnableFeed}},
'rssURLPrefix': '{{$.root.RepoLink}}/rss/branch/',
font-size: 18px;
margin-left: 4px;
}
+
+#cherry-pick-modal .scrolling.menu {
+ max-height: 200px;
+}
</span>
<svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
</button>
- <div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>
+ <div class="menu transition" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak>
<div class="ui icon search input">
<i class="icon"><svg-icon name="octicon-filter" :size="16"/></i>
<input name="search" ref="searchField" autocomplete="off" v-model="searchTerm" @keydown="keydown($event)" :placeholder="searchFieldPlaceholder">
<div class="header branch-tag-choice">
<div class="ui grid">
<div class="two column row">
- <a class="reference column" href="#" @click="createTag = false; mode = 'branches'; focusSearchField()">
+ <a class="reference column" href="#" @click="handleTabSwitch('branches')">
<span class="text" :class="{black: mode === 'branches'}">
<svg-icon name="octicon-git-branch" :size="16" class-name="gt-mr-2"/>{{ textBranches }}
</span>
</a>
<template v-if="!noTag">
- <a class="reference column" href="#" @click="createTag = true; mode = 'tags'; focusSearchField()">
+ <a class="reference column" href="#" @click="handleTabSwitch('tags')">
<span class="text" :class="{black: mode === 'tags'}">
<svg-icon name="octicon-tag" :size="16" class-name="gt-mr-2"/>{{ textTags }}
</span>
</div>
</template>
<div class="scrolling menu" ref="scrollContainer">
+ <svg-icon name="octicon-rss" symbol-id="svg-symbol-octicon-rss"/>
+ <div class="loading-indicator is-loading" v-if="isLoading"/>
<div v-for="(item, index) in filteredItems" :key="item.name" class="item" :class="{selected: item.selected, active: active === index}" @click="selectItem(item)" :ref="'listItem' + index">
{{ item.name }}
- <a v-if="enableFeed && mode === 'branches'" role="button" class="rss-icon ui compact right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
- <svg-icon name="octicon-rss" :size="14"/>
+ <a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon ui compact right" :href="rssURLPrefix + item.url" target="_blank" @click.stop>
+ <!-- creating a lot of Vue component is pretty slow, so we use a static SVG here -->
+ <svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg>
</a>
</div>
<div class="item" v-if="showCreateNewBranch" :class="{active: active === filteredItems.length}" :ref="'listItem' + filteredItems.length">
<a href="#" @click="createNewBranch()">
- <div v-show="createTag">
+ <div v-show="shouldCreateTag">
<i class="reference tags icon"/>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="textCreateTag.replace('%s', searchTerm)"/>
</div>
- <div v-show="!createTag">
+ <div v-show="!shouldCreateTag">
<svg-icon name="octicon-git-branch"/>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="textCreateBranch.replace('%s', searchTerm)"/>
<form ref="newBranchForm" :action="formActionUrl" method="post">
<input type="hidden" name="_csrf" :value="csrfToken">
<input type="hidden" name="new_branch_name" v-model="searchTerm">
- <input type="hidden" name="create_tag" v-model="createTag">
+ <input type="hidden" name="create_tag" v-model="shouldCreateTag">
<input type="hidden" name="current_path" v-model="treePath" v-if="treePath">
</form>
</div>
</div>
- <div class="message" v-if="showNoResults">
+ <div class="message" v-if="showNoResults && !isLoading">
{{ noResults }}
</div>
</div>
import $ from 'jquery';
import {SvgIcon} from '../svg.js';
import {pathEscapeSegments} from '../utils/url.js';
+import {showErrorToast} from '../modules/toast.js';
const sfc = {
components: {SvgIcon},
formActionUrl() {
return `${this.repoLink}/branches/_new/${this.branchNameSubURL}`;
},
+ shouldCreateTag() {
+ return this.mode === 'tags';
+ }
},
watch: {
menuVisible(visible) {
if (visible) {
this.focusSearchField();
+ this.fetchBranchesOrTags();
}
}
},
}
});
},
-
methods: {
selectItem(item) {
const prev = this.getSelected();
event.preventDefault();
this.menuVisible = false;
}
- }
+ },
+ handleTabSwitch(mode) {
+ if (this.isLoading) return;
+ this.mode = mode;
+ this.focusSearchField();
+ this.fetchBranchesOrTags();
+ },
+ async fetchBranchesOrTags() {
+ if (!['branches', 'tags'].includes(this.mode) || this.isLoading) return;
+ // only fetch when branch/tag list has not been initialized
+ if (this.hasListInitialized[this.mode] ||
+ (this.mode === 'branches' && !this.showBranchesInDropdown) ||
+ (this.mode === 'tags' && this.noTag)
+ ) {
+ return;
+ }
+ this.isLoading = true;
+ try {
+ // the "data.defaultBranch" is ambiguous, it could be "branch name" or "tag name"
+ const reqUrl = `${this.repoLink}/${this.mode}/list`;
+ const resp = await fetch(reqUrl);
+ const {results} = await resp.json();
+ for (const result of results) {
+ let selected = false;
+ if (this.mode === 'branches') {
+ selected = result === this.defaultBranch;
+ } else {
+ selected = result === (this.release ? this.release.tagName : this.defaultBranch);
+ }
+ this.items.push({name: result, url: pathEscapeSegments(result), branch: this.mode === 'branches', tag: this.mode === 'tags', selected});
+ }
+ this.hasListInitialized[this.mode] = true;
+ } catch (e) {
+ showErrorToast(`Network error when fetching ${this.mode}, error: ${e}`);
+ } finally {
+ this.isLoading = false;
+ }
+ },
}
};
searchTerm: '',
refNameText: '',
menuVisible: false,
- createTag: false,
release: null,
isViewTag: false,
isViewTree: false,
active: 0,
-
+ isLoading: false,
+ // This means whether branch list/tag list has initialized
+ hasListInitialized: {
+ 'branches': false,
+ 'tags': false,
+ },
...window.config.pageData.branchDropdownDataList[elIndex],
};
- // the "data.defaultBranch" is ambiguous, it could be "branch name" or "tag name"
-
- if (data.showBranchesInDropdown && data.branches) {
- for (const branch of data.branches) {
- data.items.push({name: branch, url: pathEscapeSegments(branch), branch: true, tag: false, selected: branch === data.defaultBranch});
- }
- }
- if (!data.noTag && data.tags) {
- for (const tag of data.tags) {
- if (data.release) {
- data.items.push({name: tag, url: pathEscapeSegments(tag), branch: false, tag: true, selected: tag === data.release.tagName});
- } else {
- data.items.push({name: tag, url: pathEscapeSegments(tag), branch: false, tag: true, selected: tag === data.defaultBranch});
- }
- }
- }
-
const comp = {...sfc, data() { return data }};
createApp(comp).mount(elRoot);
}
.menu .item:hover .rss-icon {
display: inline-block;
}
+
+.scrolling.menu .loading-indicator {
+ height: 4em;
+}
</style>
import {htmlEscape} from 'escape-goat';
import {svg} from '../svg.js';
+import Toastify from 'toastify-js';
const levels = {
info: {
async function showToast(message, level, {gravity, position, duration, ...other} = {}) {
if (!message) return;
- const {default: Toastify} = await import(/* webpackChunkName: 'toastify' */'toastify-js');
const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
const toast = Toastify({
name: {type: String, required: true},
size: {type: Number, default: 16},
className: {type: String, default: ''},
+ symbolId: {type: String}
},
render() {
- const {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
+ let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name);
// https://vuejs.org/guide/extras/render-function.html#creating-vnodes
// the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc
const attrs = {};
if (this.className) {
classes.push(...this.className.split(/\s+/).filter(Boolean));
}
-
+ if (this.symbolId) {
+ classes.push('gt-hidden', 'svg-symbol-container');
+ svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`;
+ }
// create VNode
return h('svg', {
...attrs,