diff --git a/models/user/user_system.go b/models/user/user_system.go index 6fbfd9e69e..e07274d291 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -10,8 +10,8 @@ import ( ) const ( - GhostUserID = -1 - GhostUserName = "Ghost" + GhostUserID int64 = -1 + GhostUserName = "Ghost" ) // NewGhostUser creates and returns a fake user for someone has deleted their account. @@ -36,9 +36,9 @@ func (u *User) IsGhost() bool { } const ( - ActionsUserID = -2 - ActionsUserName = "gitea-actions" - ActionsUserEmail = "teabot@gitea.io" + ActionsUserID int64 = -2 + ActionsUserName = "gitea-actions" + ActionsUserEmail = "teabot@gitea.io" ) func IsGiteaActionsUserName(name string) bool { diff --git a/modules/json/json.go b/modules/json/json.go index 34568c75c6..acd4118573 100644 --- a/modules/json/json.go +++ b/modules/json/json.go @@ -145,6 +145,12 @@ func Valid(data []byte) bool { // UnmarshalHandleDoubleEncode - due to a bug in xorm (see https://gitea.com/xorm/xorm/pulls/1957) - it's // possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe. func UnmarshalHandleDoubleEncode(bs []byte, v any) error { + if len(bs) == 0 { + // json.Unmarshal will report errors if input is empty (nil or zero-length) + // It seems that XORM ignores the nil but still passes zero-length string into this function + // To be consistent, we should treat all empty inputs as success + return nil + } err := json.Unmarshal(bs, v) if err != nil { ok := true diff --git a/modules/json/json_test.go b/modules/json/json_test.go new file mode 100644 index 0000000000..ace7167913 --- /dev/null +++ b/modules/json/json_test.go @@ -0,0 +1,18 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package json + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGiteaDBJSONUnmarshal(t *testing.T) { + var m map[any]any + err := UnmarshalHandleDoubleEncode(nil, &m) + assert.NoError(t, err) + err = UnmarshalHandleDoubleEncode([]byte(""), &m) + assert.NoError(t, err) +} diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index 9edf18d58e..f003742a94 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -26,7 +26,7 @@ func TestUserIDFromToken(t *testing.T) { o := OAuth2{} uid := o.userIDFromToken(t.Context(), token, ds) - assert.Equal(t, int64(user_model.ActionsUserID), uid) + assert.Equal(t, user_model.ActionsUserID, uid) assert.Equal(t, true, ds["IsActionsToken"]) assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) }) diff --git a/services/doctor/fix16961_test.go b/services/doctor/fix16961_test.go index 498ed9c8d5..d5eded1117 100644 --- a/services/doctor/fix16961_test.go +++ b/services/doctor/fix16961_test.go @@ -18,12 +18,6 @@ func Test_fixUnitConfig_16961(t *testing.T) { wantFixed bool wantErr bool }{ - { - name: "empty", - bs: "", - wantFixed: true, - wantErr: false, - }, { name: "normal: {}", bs: "{}", diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go index efc5b960cf..4bed8e2f6c 100644 --- a/services/migrations/gitlab.go +++ b/services/migrations/gitlab.go @@ -16,6 +16,7 @@ import ( "time" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" @@ -535,11 +536,15 @@ func (g *GitlabDownloader) GetComments(ctx context.Context, commentable base.Com } for _, stateEvent := range stateEvents { + posterUserID, posterUsername := user.GhostUserID, user.GhostUserName + if stateEvent.User != nil { + posterUserID, posterUsername = int64(stateEvent.User.ID), stateEvent.User.Username + } comment := &base.Comment{ IssueIndex: commentable.GetLocalIndex(), Index: int64(stateEvent.ID), - PosterID: int64(stateEvent.User.ID), - PosterName: stateEvent.User.Username, + PosterID: posterUserID, + PosterName: posterUsername, Content: "", Created: *stateEvent.CreatedAt, } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 5b6feccb8d..9a5c6ffb0f 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" @@ -246,6 +247,19 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } } + var enableRepoUnits []repo_model.RepoUnit + if opts.Releases && !unit_model.TypeReleases.UnitGlobalDisabled() { + enableRepoUnits = append(enableRepoUnits, repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeReleases}) + } + if opts.Wiki && !unit_model.TypeWiki.UnitGlobalDisabled() { + enableRepoUnits = append(enableRepoUnits, repo_model.RepoUnit{RepoID: repo.ID, Type: unit_model.TypeWiki}) + } + if len(enableRepoUnits) > 0 { + err = UpdateRepositoryUnits(ctx, repo, enableRepoUnits, nil) + if err != nil { + return nil, err + } + } return repo, committer.Commit() } diff --git a/tests/integration/mirror_pull_test.go b/tests/integration/mirror_pull_test.go index cf6faa7704..7ab8f72b4a 100644 --- a/tests/integration/mirror_pull_test.go +++ b/tests/integration/mirror_pull_test.go @@ -4,10 +4,12 @@ package integration import ( + "slices" "testing" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -19,11 +21,13 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMirrorPull(t *testing.T) { defer tests.PrepareTestEnv(t)() + ctx := t.Context() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repoPath := repo_model.RepoPath(user.Name, repo.Name) @@ -35,10 +39,10 @@ func TestMirrorPull(t *testing.T) { Mirror: true, CloneAddr: repoPath, Wiki: true, - Releases: false, + Releases: true, } - mirrorRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{ + mirrorRepo, err := repo_service.CreateRepositoryDirectly(ctx, user, user, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, IsPrivate: opts.Private, @@ -48,11 +52,15 @@ func TestMirrorPull(t *testing.T) { assert.NoError(t, err) assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation") - ctx := t.Context() - - mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil) + mirrorRepo, err = repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil) assert.NoError(t, err) + // these units should have been enabled + mirrorRepo.Units = nil + require.NoError(t, mirrorRepo.LoadUnits(ctx)) + assert.True(t, slices.ContainsFunc(mirrorRepo.Units, func(u *repo_model.RepoUnit) bool { return u.Type == unit.TypeReleases })) + assert.True(t, slices.ContainsFunc(mirrorRepo.Units, func(u *repo_model.RepoUnit) bool { return u.Type == unit.TypeWiki })) + gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo) assert.NoError(t, err) defer gitRepo.Close() @@ -60,10 +68,11 @@ func TestMirrorPull(t *testing.T) { findOptions := repo_model.FindReleasesOptions{ IncludeDrafts: true, IncludeTags: true, - RepoID: mirror.ID, + RepoID: mirrorRepo.ID, } initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions) assert.NoError(t, err) + 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 assert.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{ RepoID: repo.ID, @@ -79,12 +88,15 @@ func TestMirrorPull(t *testing.T) { IsTag: true, }, nil, "")) - _, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID) + _, err = repo_model.GetMirrorByRepoID(ctx, mirrorRepo.ID) assert.NoError(t, err) - ok := mirror_service.SyncPullMirror(ctx, mirror.ID) + ok := mirror_service.SyncPullMirror(ctx, mirrorRepo.ID) assert.True(t, ok) + // actually there is a tag in the source repo, so after "sync", that tag will also come into the mirror + initCount++ + count, err := db.Count[repo_model.Release](db.DefaultContext, findOptions) assert.NoError(t, err) assert.EqualValues(t, initCount+1, count) @@ -93,7 +105,7 @@ func TestMirrorPull(t *testing.T) { assert.NoError(t, err) assert.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true)) - ok = mirror_service.SyncPullMirror(ctx, mirror.ID) + ok = mirror_service.SyncPullMirror(ctx, mirrorRepo.ID) assert.True(t, ok) count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions) diff --git a/web_src/js/features/repo-common.test.ts b/web_src/js/features/repo-common.test.ts index 009dfc86b1..33a29ecb2c 100644 --- a/web_src/js/features/repo-common.test.ts +++ b/web_src/js/features/repo-common.test.ts @@ -1,7 +1,22 @@ -import {substituteRepoOpenWithUrl} from './repo-common.ts'; +import {sanitizeRepoName, substituteRepoOpenWithUrl} from './repo-common.ts'; test('substituteRepoOpenWithUrl', () => { // For example: "x-github-client://openRepo/https://github.com/go-gitea/gitea" expect(substituteRepoOpenWithUrl('proto://a/{url}', 'https://gitea')).toEqual('proto://a/https://gitea'); expect(substituteRepoOpenWithUrl('proto://a?link={url}', 'https://gitea')).toEqual('proto://a?link=https%3A%2F%2Fgitea'); }); + +test('sanitizeRepoName', () => { + expect(sanitizeRepoName(' a b ')).toEqual('a-b'); + expect(sanitizeRepoName('a-b_c.git ')).toEqual('a-b_c'); + expect(sanitizeRepoName('/x.git/')).toEqual('-x.git-'); + expect(sanitizeRepoName('.profile')).toEqual('.profile'); + expect(sanitizeRepoName('.profile.')).toEqual('.profile'); + expect(sanitizeRepoName('.pro..file')).toEqual('.pro.file'); + + expect(sanitizeRepoName('foo.rss.atom.git.wiki')).toEqual('foo'); + + expect(sanitizeRepoName('.')).toEqual(''); + expect(sanitizeRepoName('..')).toEqual(''); + expect(sanitizeRepoName('-')).toEqual(''); +}); diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 4362a2c713..ebb6881c67 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -159,3 +159,19 @@ export async function updateIssuesMeta(url: string, action: string, issue_ids: s console.error(error); } } + +export function sanitizeRepoName(name: string): string { + name = name.trim().replace(/[^-.\w]/g, '-'); + for (let lastName = ''; lastName !== name;) { + lastName = name; + name = name.replace(/\.+$/g, ''); + name = name.replace(/\.{2,}/g, '.'); + for (const ext of ['.git', '.wiki', '.rss', '.atom']) { + if (name.endsWith(ext)) { + name = name.substring(0, name.length - ext.length); + } + } + } + if (['.', '..', '-'].includes(name)) name = ''; + return name; +} diff --git a/web_src/js/features/repo-migration.ts b/web_src/js/features/repo-migration.ts index fb9c822f98..4914e47267 100644 --- a/web_src/js/features/repo-migration.ts +++ b/web_src/js/features/repo-migration.ts @@ -1,4 +1,5 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.ts'; +import {sanitizeRepoName} from './repo-common.ts'; const service = document.querySelector('#service_type'); const user = document.querySelector('#auth_username'); @@ -25,13 +26,19 @@ export function initRepoMigration() { }); lfs?.addEventListener('change', setLFSSettingsVisibility); - const cloneAddr = document.querySelector('#clone_addr'); - cloneAddr?.addEventListener('change', () => { - const repoName = document.querySelector('#repo_name'); - if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank - repoName.value = /^(.*\/)?((.+?)(\.git)?)$/.exec(cloneAddr.value)[3]; - } - }); + const elCloneAddr = document.querySelector('#clone_addr'); + const elRepoName = document.querySelector('#repo_name'); + if (elCloneAddr && elRepoName) { + let repoNameChanged = false; + elRepoName.addEventListener('input', () => {repoNameChanged = true}); + elCloneAddr.addEventListener('input', () => { + if (repoNameChanged) return; + let repoNameFromUrl = elCloneAddr.value.split(/[?#]/)[0]; + repoNameFromUrl = /^(.*\/)?((.+?)\/?)$/.exec(repoNameFromUrl)[3]; + repoNameFromUrl = repoNameFromUrl.split(/[?#]/)[0]; + elRepoName.value = sanitizeRepoName(repoNameFromUrl); + }); + } } function checkAuth() { diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index f2c5eba62c..5128942465 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -1,6 +1,7 @@ import {hideElem, showElem, toggleElem} from '../utils/dom.ts'; import {htmlEscape} from 'escape-goat'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import {sanitizeRepoName} from './repo-common.ts'; const {appSubUrl} = window.config; @@ -74,6 +75,10 @@ export function initRepoNew() { } }; inputRepoName.addEventListener('input', updateUiRepoName); + inputRepoName.addEventListener('change', () => { + inputRepoName.value = sanitizeRepoName(inputRepoName.value); + updateUiRepoName(); + }); updateUiRepoName(); initRepoNewTemplateSearch(form);