Skip to content

Commit 441acf6

Browse files
committed
Merge remote-tracking branch 'giteaofficial/main'
* giteaofficial/main: [skip ci] Updated translations via Crowdin Add ff_only parameter to POST /repos/{owner}/{repo}/merge-upstream (go-gitea#34770) Add repo file tree item link behavior (go-gitea#34730) Fix tag target (go-gitea#34781)
2 parents 8f883a1 + d462ce1 commit 441acf6

File tree

13 files changed

+136
-111
lines changed

13 files changed

+136
-111
lines changed

modules/structs/repo_branch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ type UpdateBranchProtectionPriories struct {
136136

137137
type MergeUpstreamRequest struct {
138138
Branch string `json:"branch"`
139+
FfOnly bool `json:"ff_only"`
139140
}
140141

141142
type MergeUpstreamResponse struct {

options/locale/locale_ga-IE.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,7 +1332,9 @@ editor.upload_file=Uaslódáil Comhad
13321332
editor.edit_file=Cuir Comhad in eagar
13331333
editor.preview_changes=Athruithe Réamhamhar
13341334
editor.cannot_edit_lfs_files=Ní féidir comhaid LFS a chur in eagar sa chomhéadan gréasáin.
1335+
editor.cannot_edit_too_large_file=Tá an comhad rómhór le cur in eagar.
13351336
editor.cannot_edit_non_text_files=Ní féidir comhaid dhénártha a chur in eagar sa chomhéadan gréasáin.
1337+
editor.file_not_editable_hint=Ach is féidir leat é a athainmniú nó a bhogadh fós.
13361338
editor.edit_this_file=Cuir Comhad in eagar
13371339
editor.this_file_locked=Tá an comhad faoi ghlas
13381340
editor.must_be_on_a_branch=Caithfidh tú a bheith ar bhrainse chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo.

routers/api/v1/repo/branch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,7 +1181,7 @@ func MergeUpstream(ctx *context.APIContext) {
11811181
// "404":
11821182
// "$ref": "#/responses/notFound"
11831183
form := web.GetForm(ctx).(*api.MergeUpstreamRequest)
1184-
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch)
1184+
mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly)
11851185
if err != nil {
11861186
if errors.Is(err, util.ErrInvalidArgument) {
11871187
ctx.APIError(http.StatusBadRequest, err)

routers/web/repo/branch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ func CreateBranch(ctx *context.Context) {
258258

259259
func MergeUpstream(ctx *context.Context) {
260260
branchName := ctx.FormString("branch")
261-
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
261+
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
262262
if err != nil {
263263
if errors.Is(err, util.ErrNotExist) {
264264
ctx.JSONErrorNotFound()

routers/web/repo/release.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ func NewRelease(ctx *context.Context) {
381381

382382
ctx.Data["ShowCreateTagOnlyButton"] = false
383383
ctx.Data["tag_name"] = rel.TagName
384-
ctx.Data["tag_target"] = rel.Target
384+
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
385385
ctx.Data["title"] = rel.Title
386386
ctx.Data["content"] = rel.Note
387387
ctx.Data["attachments"] = rel.Attachments
@@ -537,7 +537,7 @@ func EditRelease(ctx *context.Context) {
537537
}
538538
ctx.Data["ID"] = rel.ID
539539
ctx.Data["tag_name"] = rel.TagName
540-
ctx.Data["tag_target"] = rel.Target
540+
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
541541
ctx.Data["title"] = rel.Title
542542
ctx.Data["content"] = rel.Note
543543
ctx.Data["prerelease"] = rel.IsPrerelease
@@ -583,7 +583,7 @@ func EditReleasePost(ctx *context.Context) {
583583
return
584584
}
585585
ctx.Data["tag_name"] = rel.TagName
586-
ctx.Data["tag_target"] = rel.Target
586+
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
587587
ctx.Data["title"] = rel.Title
588588
ctx.Data["content"] = rel.Note
589589
ctx.Data["prerelease"] = rel.IsPrerelease

services/repository/merge_upstream.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
)
1919

2020
// MergeUpstream merges the base repository's default branch into the fork repository's current branch.
21-
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) {
21+
func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string, ffOnly bool) (mergeStyle string, err error) {
2222
if err = repo.MustNotBeArchived(); err != nil {
2323
return "", err
2424
}
@@ -45,6 +45,11 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_
4545
return "", err
4646
}
4747

48+
// If ff_only is requested and fast-forward failed, return error
49+
if ffOnly {
50+
return "", util.NewInvalidArgumentErrorf("fast-forward merge not possible: branch has diverged")
51+
}
52+
4853
// TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment
4954
// ideally in the future the "merge" functions should be refactored to decouple from the PullRequest
5055
fakeIssue := &issue_model.Issue{

templates/swagger/v1_json.tmpl

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/repo_merge_upstream_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,37 @@ func TestRepoMergeUpstream(t *testing.T) {
147147
return queryMergeUpstreamButtonLink(htmlDoc) == ""
148148
}, 5*time.Second, 100*time.Millisecond)
149149
})
150+
151+
t.Run("FastForwardOnly", func(t *testing.T) {
152+
// Create a clean branch for fast-forward testing
153+
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{
154+
"_csrf": GetUserCSRFToken(t, session),
155+
"new_branch_name": "ff-test-branch",
156+
})
157+
session.MakeRequest(t, req, http.StatusSeeOther)
158+
159+
// Add content to base repository that can be fast-forwarded
160+
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "ff-test.txt", "master", "ff-content-1"))
161+
162+
// ff_only=true with fast-forward possible (should succeed)
163+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
164+
Branch: "ff-test-branch",
165+
FfOnly: true,
166+
}).AddTokenAuth(token)
167+
resp := MakeRequest(t, req, http.StatusOK)
168+
169+
var mergeResp api.MergeUpstreamResponse
170+
DecodeJSON(t, resp, &mergeResp)
171+
assert.Equal(t, "fast-forward", mergeResp.MergeStyle)
172+
173+
// ff_only=true when fast-forward is not possible (should fail)
174+
require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "another-file.txt", "master", "more-content"))
175+
176+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{
177+
Branch: "fork-branch",
178+
FfOnly: true,
179+
}).AddTokenAuth(token)
180+
MakeRequest(t, req, http.StatusBadRequest)
181+
})
150182
})
151183
}

web_src/js/components/DiffFileTree.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ function updateState(visible: boolean) {
6060
</script>
6161

6262
<template>
63+
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
6364
<div v-if="store.fileTreeIsVisible" class="diff-file-tree-items">
64-
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
6565
<DiffFileTreeItem v-for="item in store.diffFileTree.TreeRoot.Children" :key="item.FullName" :item="item"/>
6666
</div>
6767
</template>

web_src/js/components/ViewFileTree.vue

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
<script lang="ts" setup>
22
import ViewFileTreeItem from './ViewFileTreeItem.vue';
33
import {onMounted, ref} from 'vue';
4-
import {pathEscapeSegments} from '../utils/url.ts';
5-
import {GET} from '../modules/fetch.ts';
6-
import {createElementFromHTML} from '../utils/dom.ts';
4+
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
75
86
const elRoot = ref<HTMLElement | null>(null);
97
@@ -13,52 +11,20 @@ const props = defineProps({
1311
currentRefNameSubURL: {type: String, required: true},
1412
});
1513
16-
const files = ref([]);
17-
const selectedItem = ref('');
18-
19-
async function loadChildren(treePath: string, subPath: string = '') {
20-
const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`);
21-
const json = await response.json();
22-
const poolSvgs = [];
23-
for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) {
24-
if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
25-
}
26-
if (poolSvgs.length) {
27-
const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>');
28-
svgContainer.innerHTML = poolSvgs.join('');
29-
document.body.append(svgContainer);
30-
}
31-
return json.fileTreeNodes ?? null;
32-
}
33-
34-
async function loadViewContent(url: string) {
35-
url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`;
36-
const response = await GET(url);
37-
document.querySelector('.repo-view-content').innerHTML = await response.text();
38-
}
39-
40-
async function navigateTreeView(treePath: string) {
41-
const url = `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
42-
window.history.pushState({treePath, url}, null, url);
43-
selectedItem.value = treePath;
44-
await loadViewContent(url);
45-
}
46-
14+
const store = createViewFileTreeStore(props);
4715
onMounted(async () => {
48-
selectedItem.value = props.treePath;
49-
files.value = await loadChildren('', props.treePath);
16+
store.rootFiles = await store.loadChildren('', props.treePath);
5017
elRoot.value.closest('.is-loading')?.classList?.remove('is-loading');
5118
window.addEventListener('popstate', (e) => {
52-
selectedItem.value = e.state?.treePath || '';
53-
if (e.state?.url) loadViewContent(e.state.url);
19+
store.selectedItem = e.state?.treePath || '';
20+
if (e.state?.url) store.loadViewContent(e.state.url);
5421
});
5522
});
5623
</script>
5724

5825
<template>
5926
<div class="view-file-tree-items" ref="elRoot">
60-
<!-- only render the tree if we're visible. in many cases this is something that doesn't change very often -->
61-
<ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :navigate-view-content="navigateTreeView" :load-children="loadChildren"/>
27+
<ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/>
6228
</div>
6329
</template>
6430

web_src/js/components/ViewFileTreeItem.vue

Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script lang="ts" setup>
22
import {SvgIcon} from '../svg.ts';
3+
import {isPlainClick} from '../utils/dom.ts';
34
import {ref} from 'vue';
5+
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
46
57
type Item = {
68
entryName: string;
7-
entryMode: string;
9+
entryMode: 'blob' | 'exec' | 'tree' | 'commit' | 'symlink' | 'unknown';
810
entryIcon: string;
911
entryIconOpen: string;
1012
fullPath: string;
@@ -14,103 +16,67 @@ type Item = {
1416
1517
const props = defineProps<{
1618
item: Item,
17-
navigateViewContent:(treePath: string) => void,
18-
loadChildren:(treePath: string, subPath?: string) => Promise<Item[]>,
19-
selectedItem?: string,
19+
store: ReturnType<typeof createViewFileTreeStore>
2020
}>();
2121
22+
const store = props.store;
2223
const isLoading = ref(false);
2324
const children = ref(props.item.children);
2425
const collapsed = ref(!props.item.children);
2526
2627
const doLoadChildren = async () => {
2728
collapsed.value = !collapsed.value;
28-
if (!collapsed.value && props.loadChildren) {
29+
if (!collapsed.value) {
2930
isLoading.value = true;
3031
try {
31-
children.value = await props.loadChildren(props.item.fullPath);
32+
children.value = await store.loadChildren(props.item.fullPath);
3233
} finally {
3334
isLoading.value = false;
3435
}
3536
}
3637
};
3738
38-
const doLoadDirContent = () => {
39-
doLoadChildren();
40-
props.navigateViewContent(props.item.fullPath);
39+
const onItemClick = (e: MouseEvent) => {
40+
// only handle the click event with page partial reloading if the user didn't press any special key
41+
// let browsers handle special keys like "Ctrl+Click"
42+
if (!isPlainClick(e)) return;
43+
e.preventDefault();
44+
if (props.item.entryMode === 'tree') doLoadChildren();
45+
store.navigateTreeView(props.item.fullPath);
4146
};
4247
43-
const doLoadFileContent = () => {
44-
props.navigateViewContent(props.item.fullPath);
45-
};
46-
47-
const doGotoSubModule = () => {
48-
location.href = props.item.submoduleUrl;
49-
};
5048
</script>
5149

52-
<!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"-->
5350
<template>
54-
<div
55-
v-if="item.entryMode === 'commit'" class="tree-item type-submodule"
56-
:title="item.entryName"
57-
@click.stop="doGotoSubModule"
58-
>
59-
<!-- submodule -->
60-
<div class="item-content">
61-
<!-- eslint-disable-next-line vue/no-v-html -->
62-
<span class="tw-contents" v-html="item.entryIcon"/>
63-
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
64-
</div>
65-
</div>
66-
<div
67-
v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink"
68-
:class="{'selected': selectedItem === item.fullPath}"
51+
<a
52+
class="tree-item silenced"
53+
:class="{
54+
'selected': store.selectedItem === item.fullPath,
55+
'type-submodule': item.entryMode === 'commit',
56+
'type-directory': item.entryMode === 'tree',
57+
'type-symlink': item.entryMode === 'symlink',
58+
'type-file': item.entryMode === 'blob' || item.entryMode === 'exec',
59+
}"
6960
:title="item.entryName"
70-
@click.stop="doLoadFileContent"
61+
:href="store.buildTreePathWebUrl(item.fullPath)"
62+
@click.stop="onItemClick"
7163
>
72-
<!-- symlink -->
73-
<div class="item-content">
74-
<!-- eslint-disable-next-line vue/no-v-html -->
75-
<span class="tw-contents" v-html="item.entryIcon"/>
76-
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
77-
</div>
78-
</div>
79-
<div
80-
v-else-if="item.entryMode !== 'tree'" class="tree-item type-file"
81-
:class="{'selected': selectedItem === item.fullPath}"
82-
:title="item.entryName"
83-
@click.stop="doLoadFileContent"
84-
>
85-
<!-- file -->
86-
<div class="item-content">
87-
<!-- eslint-disable-next-line vue/no-v-html -->
88-
<span class="tw-contents" v-html="item.entryIcon"/>
89-
<span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span>
90-
</div>
91-
</div>
92-
<div
93-
v-else class="tree-item type-directory"
94-
:class="{'selected': selectedItem === item.fullPath}"
95-
:title="item.entryName"
96-
@click.stop="doLoadDirContent"
97-
>
98-
<!-- directory -->
99-
<div class="item-toggle">
64+
<div v-if="item.entryMode === 'tree'" class="item-toggle">
10065
<SvgIcon v-if="isLoading" name="octicon-sync" class="circular-spin"/>
101-
<SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/>
66+
<SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop.prevent="doLoadChildren"/>
10267
</div>
10368
<div class="item-content">
10469
<!-- eslint-disable-next-line vue/no-v-html -->
10570
<span class="tw-contents" v-html="(!collapsed && item.entryIconOpen) ? item.entryIconOpen : item.entryIcon"/>
10671
<span class="gt-ellipsis">{{ item.entryName }}</span>
10772
</div>
108-
</div>
73+
</a>
10974

11075
<div v-if="children?.length" v-show="!collapsed" class="sub-items">
111-
<ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :selected-item="selectedItem" :navigate-view-content="navigateViewContent" :load-children="loadChildren"/>
76+
<ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :store="store"/>
11277
</div>
11378
</template>
79+
11480
<style scoped>
11581
.sub-items {
11682
display: flex;

0 commit comments

Comments
 (0)