Skip to content

Commit 51d86ad

Browse files
authored
Fix some migration and repo name problems (#33986)
1. Ignore empty inputs in `UnmarshalHandleDoubleEncode` 2. Ignore non-existing `stateEvent.User` in gitlab migration 3. Enable `release` and `wiki` units when they are selected in migration 4. Sanitize repo name for migration and new repo
1 parent 536f4c6 commit 51d86ad

File tree

12 files changed

+123
-31
lines changed

12 files changed

+123
-31
lines changed

models/user/user_system.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
)
1111

1212
const (
13-
GhostUserID = -1
14-
GhostUserName = "Ghost"
13+
GhostUserID int64 = -1
14+
GhostUserName = "Ghost"
1515
)
1616

1717
// NewGhostUser creates and returns a fake user for someone has deleted their account.
@@ -36,9 +36,9 @@ func (u *User) IsGhost() bool {
3636
}
3737

3838
const (
39-
ActionsUserID = -2
40-
ActionsUserName = "gitea-actions"
41-
ActionsUserEmail = "[email protected]"
39+
ActionsUserID int64 = -2
40+
ActionsUserName = "gitea-actions"
41+
ActionsUserEmail = "[email protected]"
4242
)
4343

4444
func IsGiteaActionsUserName(name string) bool {

modules/json/json.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ func Valid(data []byte) bool {
145145
// UnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's
146146
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
147147
func UnmarshalHandleDoubleEncode(bs []byte, v any) error {
148+
if len(bs) == 0 {
149+
// json.Unmarshal will report errors if input is empty (nil or zero-length)
150+
// It seems that XORM ignores the nil but still passes zero-length string into this function
151+
// To be consistent, we should treat all empty inputs as success
152+
return nil
153+
}
148154
err := json.Unmarshal(bs, v)
149155
if err != nil {
150156
ok := true

modules/json/json_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package json
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGiteaDBJSONUnmarshal(t *testing.T) {
13+
var m map[any]any
14+
err := UnmarshalHandleDoubleEncode(nil, &m)
15+
assert.NoError(t, err)
16+
err = UnmarshalHandleDoubleEncode([]byte(""), &m)
17+
assert.NoError(t, err)
18+
}

services/auth/oauth2_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestUserIDFromToken(t *testing.T) {
2626

2727
o := OAuth2{}
2828
uid := o.userIDFromToken(t.Context(), token, ds)
29-
assert.Equal(t, int64(user_model.ActionsUserID), uid)
29+
assert.Equal(t, user_model.ActionsUserID, uid)
3030
assert.Equal(t, true, ds["IsActionsToken"])
3131
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
3232
})

services/doctor/fix16961_test.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@ func Test_fixUnitConfig_16961(t *testing.T) {
1818
wantFixed bool
1919
wantErr bool
2020
}{
21-
{
22-
name: "empty",
23-
bs: "",
24-
wantFixed: true,
25-
wantErr: false,
26-
},
2721
{
2822
name: "normal: {}",
2923
bs: "{}",

services/migrations/gitlab.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
issues_model "code.gitea.io/gitea/models/issues"
19+
"code.gitea.io/gitea/models/user"
1920
"code.gitea.io/gitea/modules/container"
2021
"code.gitea.io/gitea/modules/log"
2122
base "code.gitea.io/gitea/modules/migration"
@@ -535,11 +536,15 @@ func (g *GitlabDownloader) GetComments(ctx context.Context, commentable base.Com
535536
}
536537

537538
for _, stateEvent := range stateEvents {
539+
posterUserID, posterUsername := user.GhostUserID, user.GhostUserName
540+
if stateEvent.User != nil {
541+
posterUserID, posterUsername = int64(stateEvent.User.ID), stateEvent.User.Username
542+
}
538543
comment := &base.Comment{
539544
IssueIndex: commentable.GetLocalIndex(),
540545
Index: int64(stateEvent.ID),
541-
PosterID: int64(stateEvent.User.ID),
542-
PosterName: stateEvent.User.Username,
546+
PosterID: posterUserID,
547+
PosterName: posterUsername,
543548
Content: "",
544549
Created: *stateEvent.CreatedAt,
545550
}

services/repository/migrate.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"code.gitea.io/gitea/models/db"
1414
"code.gitea.io/gitea/models/organization"
1515
repo_model "code.gitea.io/gitea/models/repo"
16+
unit_model "code.gitea.io/gitea/models/unit"
1617
user_model "code.gitea.io/gitea/models/user"
1718
"code.gitea.io/gitea/modules/git"
1819
"code.gitea.io/gitea/modules/gitrepo"
@@ -246,6 +247,19 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
246247
}
247248
}
248249

250+
var enableRepoUnits []repo_model.RepoUnit
251+
if opts.Releases && !unit_model.TypeReleases.UnitGlobalDisabled() {
252+
enableRepoUnits = append(enableRepoUnits, repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeReleases})
253+
}
254+
if opts.Wiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
255+
enableRepoUnits = append(enableRepoUnits, repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeWiki})
256+
}
257+
if len(enableRepoUnits) > 0 {
258+
err = UpdateRepositoryUnits(ctx, repo, enableRepoUnits, nil)
259+
if err != nil {
260+
return nil, err
261+
}
262+
}
249263
return repo, committer.Commit()
250264
}
251265

tests/integration/mirror_pull_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
package integration
55

66
import (
7+
"slices"
78
"testing"
89

910
"code.gitea.io/gitea/models/db"
1011
repo_model "code.gitea.io/gitea/models/repo"
12+
"code.gitea.io/gitea/models/unit"
1113
"code.gitea.io/gitea/models/unittest"
1214
user_model "code.gitea.io/gitea/models/user"
1315
"code.gitea.io/gitea/modules/git"
@@ -19,11 +21,13 @@ import (
1921
"code.gitea.io/gitea/tests"
2022

2123
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
2225
)
2326

2427
func TestMirrorPull(t *testing.T) {
2528
defer tests.PrepareTestEnv(t)()
2629

30+
ctx := t.Context()
2731
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
2832
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
2933
repoPath := repo_model.RepoPath(user.Name, repo.Name)
@@ -35,10 +39,10 @@ func TestMirrorPull(t *testing.T) {
3539
Mirror: true,
3640
CloneAddr: repoPath,
3741
Wiki: true,
38-
Releases: false,
42+
Releases: true,
3943
}
4044

41-
mirrorRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
45+
mirrorRepo, err := repo_service.CreateRepositoryDirectly(ctx, user, user, repo_service.CreateRepoOptions{
4246
Name: opts.RepoName,
4347
Description: opts.Description,
4448
IsPrivate: opts.Private,
@@ -48,22 +52,27 @@ func TestMirrorPull(t *testing.T) {
4852
assert.NoError(t, err)
4953
assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation")
5054

51-
ctx := t.Context()
52-
53-
mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
55+
mirrorRepo, err = repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
5456
assert.NoError(t, err)
5557

58+
// these units should have been enabled
59+
mirrorRepo.Units = nil
60+
require.NoError(t, mirrorRepo.LoadUnits(ctx))
61+
assert.True(t, slices.ContainsFunc(mirrorRepo.Units, func(u *repo_model.RepoUnit) bool { return u.Type == unit.TypeReleases }))
62+
assert.True(t, slices.ContainsFunc(mirrorRepo.Units, func(u *repo_model.RepoUnit) bool { return u.Type == unit.TypeWiki }))
63+
5664
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
5765
assert.NoError(t, err)
5866
defer gitRepo.Close()
5967

6068
findOptions := repo_model.FindReleasesOptions{
6169
IncludeDrafts: true,
6270
IncludeTags: true,
63-
RepoID: mirror.ID,
71+
RepoID: mirrorRepo.ID,
6472
}
6573
initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
6674
assert.NoError(t, err)
75+
assert.Zero(t, initCount) // no sync yet, so even though there is a tag in source repo, the mirror's release table is still empty
6776

6877
assert.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{
6978
RepoID: repo.ID,
@@ -79,12 +88,15 @@ func TestMirrorPull(t *testing.T) {
7988
IsTag: true,
8089
}, nil, ""))
8190

82-
_, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
91+
_, err = repo_model.GetMirrorByRepoID(ctx, mirrorRepo.ID)
8392
assert.NoError(t, err)
8493

85-
ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
94+
ok := mirror_service.SyncPullMirror(ctx, mirrorRepo.ID)
8695
assert.True(t, ok)
8796

97+
// actually there is a tag in the source repo, so after "sync", that tag will also come into the mirror
98+
initCount++
99+
88100
count, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
89101
assert.NoError(t, err)
90102
assert.EqualValues(t, initCount+1, count)
@@ -93,7 +105,7 @@ func TestMirrorPull(t *testing.T) {
93105
assert.NoError(t, err)
94106
assert.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true))
95107

96-
ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
108+
ok = mirror_service.SyncPullMirror(ctx, mirrorRepo.ID)
97109
assert.True(t, ok)
98110

99111
count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions)
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1-
import {substituteRepoOpenWithUrl} from './repo-common.ts';
1+
import {sanitizeRepoName, substituteRepoOpenWithUrl} from './repo-common.ts';
22

33
test('substituteRepoOpenWithUrl', () => {
44
// For example: "x-github-client://openRepo/https://github.com/go-gitea/gitea"
55
expect(substituteRepoOpenWithUrl('proto://a/{url}', 'https://gitea')).toEqual('proto://a/https://gitea');
66
expect(substituteRepoOpenWithUrl('proto://a?link={url}', 'https://gitea')).toEqual('proto://a?link=https%3A%2F%2Fgitea');
77
});
8+
9+
test('sanitizeRepoName', () => {
10+
expect(sanitizeRepoName(' a b ')).toEqual('a-b');
11+
expect(sanitizeRepoName('a-b_c.git ')).toEqual('a-b_c');
12+
expect(sanitizeRepoName('/x.git/')).toEqual('-x.git-');
13+
expect(sanitizeRepoName('.profile')).toEqual('.profile');
14+
expect(sanitizeRepoName('.profile.')).toEqual('.profile');
15+
expect(sanitizeRepoName('.pro..file')).toEqual('.pro.file');
16+
17+
expect(sanitizeRepoName('foo.rss.atom.git.wiki')).toEqual('foo');
18+
19+
expect(sanitizeRepoName('.')).toEqual('');
20+
expect(sanitizeRepoName('..')).toEqual('');
21+
expect(sanitizeRepoName('-')).toEqual('');
22+
});

web_src/js/features/repo-common.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,19 @@ export async function updateIssuesMeta(url: string, action: string, issue_ids: s
159159
console.error(error);
160160
}
161161
}
162+
163+
export function sanitizeRepoName(name: string): string {
164+
name = name.trim().replace(/[^-.\w]/g, '-');
165+
for (let lastName = ''; lastName !== name;) {
166+
lastName = name;
167+
name = name.replace(/\.+$/g, '');
168+
name = name.replace(/\.{2,}/g, '.');
169+
for (const ext of ['.git', '.wiki', '.rss', '.atom']) {
170+
if (name.endsWith(ext)) {
171+
name = name.substring(0, name.length - ext.length);
172+
}
173+
}
174+
}
175+
if (['.', '..', '-'].includes(name)) name = '';
176+
return name;
177+
}

web_src/js/features/repo-migration.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
2+
import {sanitizeRepoName} from './repo-common.ts';
23

34
const service = document.querySelector<HTMLInputElement>('#service_type');
45
const user = document.querySelector<HTMLInputElement>('#auth_username');
@@ -25,13 +26,19 @@ export function initRepoMigration() {
2526
});
2627
lfs?.addEventListener('change', setLFSSettingsVisibility);
2728

28-
const cloneAddr = document.querySelector<HTMLInputElement>('#clone_addr');
29-
cloneAddr?.addEventListener('change', () => {
30-
const repoName = document.querySelector<HTMLInputElement>('#repo_name');
31-
if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank
32-
repoName.value = /^(.*\/)?((.+?)(\.git)?)$/.exec(cloneAddr.value)[3];
33-
}
34-
});
29+
const elCloneAddr = document.querySelector<HTMLInputElement>('#clone_addr');
30+
const elRepoName = document.querySelector<HTMLInputElement>('#repo_name');
31+
if (elCloneAddr && elRepoName) {
32+
let repoNameChanged = false;
33+
elRepoName.addEventListener('input', () => {repoNameChanged = true});
34+
elCloneAddr.addEventListener('input', () => {
35+
if (repoNameChanged) return;
36+
let repoNameFromUrl = elCloneAddr.value.split(/[?#]/)[0];
37+
repoNameFromUrl = /^(.*\/)?((.+?)\/?)$/.exec(repoNameFromUrl)[3];
38+
repoNameFromUrl = repoNameFromUrl.split(/[?#]/)[0];
39+
elRepoName.value = sanitizeRepoName(repoNameFromUrl);
40+
});
41+
}
3542
}
3643

3744
function checkAuth() {

web_src/js/features/repo-new.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
22
import {htmlEscape} from 'escape-goat';
33
import {fomanticQuery} from '../modules/fomantic/base.ts';
4+
import {sanitizeRepoName} from './repo-common.ts';
45

56
const {appSubUrl} = window.config;
67

@@ -74,6 +75,10 @@ export function initRepoNew() {
7475
}
7576
};
7677
inputRepoName.addEventListener('input', updateUiRepoName);
78+
inputRepoName.addEventListener('change', () => {
79+
inputRepoName.value = sanitizeRepoName(inputRepoName.value);
80+
updateUiRepoName();
81+
});
7782
updateUiRepoName();
7883

7984
initRepoNewTemplateSearch(form);

0 commit comments

Comments
 (0)