mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-21 17:08:43 +03:00
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
This commit is contained in:
@ -10,8 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GhostUserID = -1
|
GhostUserID int64 = -1
|
||||||
GhostUserName = "Ghost"
|
GhostUserName = "Ghost"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewGhostUser creates and returns a fake user for someone has deleted their account.
|
// NewGhostUser creates and returns a fake user for someone has deleted their account.
|
||||||
@ -36,9 +36,9 @@ func (u *User) IsGhost() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ActionsUserID = -2
|
ActionsUserID int64 = -2
|
||||||
ActionsUserName = "gitea-actions"
|
ActionsUserName = "gitea-actions"
|
||||||
ActionsUserEmail = "teabot@gitea.io"
|
ActionsUserEmail = "teabot@gitea.io"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsGiteaActionsUserName(name string) bool {
|
func IsGiteaActionsUserName(name string) bool {
|
||||||
|
@ -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
|
// 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.
|
// possible that a Blob may be double encoded or gain an unwanted prefix of 0xff 0xfe.
|
||||||
func UnmarshalHandleDoubleEncode(bs []byte, v any) error {
|
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)
|
err := json.Unmarshal(bs, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ok := true
|
ok := true
|
||||||
|
18
modules/json/json_test.go
Normal file
18
modules/json/json_test.go
Normal file
@ -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)
|
||||||
|
}
|
@ -26,7 +26,7 @@ func TestUserIDFromToken(t *testing.T) {
|
|||||||
|
|
||||||
o := OAuth2{}
|
o := OAuth2{}
|
||||||
uid := o.userIDFromToken(t.Context(), token, ds)
|
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, true, ds["IsActionsToken"])
|
||||||
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
|
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
|
||||||
})
|
})
|
||||||
|
@ -18,12 +18,6 @@ func Test_fixUnitConfig_16961(t *testing.T) {
|
|||||||
wantFixed bool
|
wantFixed bool
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
bs: "",
|
|
||||||
wantFixed: true,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "normal: {}",
|
name: "normal: {}",
|
||||||
bs: "{}",
|
bs: "{}",
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
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/container"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
base "code.gitea.io/gitea/modules/migration"
|
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 {
|
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{
|
comment := &base.Comment{
|
||||||
IssueIndex: commentable.GetLocalIndex(),
|
IssueIndex: commentable.GetLocalIndex(),
|
||||||
Index: int64(stateEvent.ID),
|
Index: int64(stateEvent.ID),
|
||||||
PosterID: int64(stateEvent.User.ID),
|
PosterID: posterUserID,
|
||||||
PosterName: stateEvent.User.Username,
|
PosterName: posterUsername,
|
||||||
Content: "",
|
Content: "",
|
||||||
Created: *stateEvent.CreatedAt,
|
Created: *stateEvent.CreatedAt,
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"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()
|
return repo, committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
@ -19,11 +21,13 @@ import (
|
|||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMirrorPull(t *testing.T) {
|
func TestMirrorPull(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
repoPath := repo_model.RepoPath(user.Name, repo.Name)
|
repoPath := repo_model.RepoPath(user.Name, repo.Name)
|
||||||
@ -35,10 +39,10 @@ func TestMirrorPull(t *testing.T) {
|
|||||||
Mirror: true,
|
Mirror: true,
|
||||||
CloneAddr: repoPath,
|
CloneAddr: repoPath,
|
||||||
Wiki: true,
|
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,
|
Name: opts.RepoName,
|
||||||
Description: opts.Description,
|
Description: opts.Description,
|
||||||
IsPrivate: opts.Private,
|
IsPrivate: opts.Private,
|
||||||
@ -48,11 +52,15 @@ func TestMirrorPull(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation")
|
assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation")
|
||||||
|
|
||||||
ctx := t.Context()
|
mirrorRepo, err = repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
|
||||||
|
|
||||||
mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
|
|
||||||
assert.NoError(t, err)
|
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)
|
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
@ -60,10 +68,11 @@ func TestMirrorPull(t *testing.T) {
|
|||||||
findOptions := repo_model.FindReleasesOptions{
|
findOptions := repo_model.FindReleasesOptions{
|
||||||
IncludeDrafts: true,
|
IncludeDrafts: true,
|
||||||
IncludeTags: true,
|
IncludeTags: true,
|
||||||
RepoID: mirror.ID,
|
RepoID: mirrorRepo.ID,
|
||||||
}
|
}
|
||||||
initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
|
initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
|
||||||
assert.NoError(t, err)
|
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{
|
assert.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
@ -79,12 +88,15 @@ func TestMirrorPull(t *testing.T) {
|
|||||||
IsTag: true,
|
IsTag: true,
|
||||||
}, nil, ""))
|
}, nil, ""))
|
||||||
|
|
||||||
_, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
|
_, err = repo_model.GetMirrorByRepoID(ctx, mirrorRepo.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
|
ok := mirror_service.SyncPullMirror(ctx, mirrorRepo.ID)
|
||||||
assert.True(t, ok)
|
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)
|
count, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, initCount+1, count)
|
assert.EqualValues(t, initCount+1, count)
|
||||||
@ -93,7 +105,7 @@ func TestMirrorPull(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true))
|
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)
|
assert.True(t, ok)
|
||||||
|
|
||||||
count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions)
|
count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions)
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
import {substituteRepoOpenWithUrl} from './repo-common.ts';
|
import {sanitizeRepoName, substituteRepoOpenWithUrl} from './repo-common.ts';
|
||||||
|
|
||||||
test('substituteRepoOpenWithUrl', () => {
|
test('substituteRepoOpenWithUrl', () => {
|
||||||
// For example: "x-github-client://openRepo/https://github.com/go-gitea/gitea"
|
// 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/{url}', 'https://gitea')).toEqual('proto://a/https://gitea');
|
||||||
expect(substituteRepoOpenWithUrl('proto://a?link={url}', 'https://gitea')).toEqual('proto://a?link=https%3A%2F%2Fgitea');
|
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('');
|
||||||
|
});
|
||||||
|
@ -159,3 +159,19 @@ export async function updateIssuesMeta(url: string, action: string, issue_ids: s
|
|||||||
console.error(error);
|
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;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||||
|
import {sanitizeRepoName} from './repo-common.ts';
|
||||||
|
|
||||||
const service = document.querySelector<HTMLInputElement>('#service_type');
|
const service = document.querySelector<HTMLInputElement>('#service_type');
|
||||||
const user = document.querySelector<HTMLInputElement>('#auth_username');
|
const user = document.querySelector<HTMLInputElement>('#auth_username');
|
||||||
@ -25,13 +26,19 @@ export function initRepoMigration() {
|
|||||||
});
|
});
|
||||||
lfs?.addEventListener('change', setLFSSettingsVisibility);
|
lfs?.addEventListener('change', setLFSSettingsVisibility);
|
||||||
|
|
||||||
const cloneAddr = document.querySelector<HTMLInputElement>('#clone_addr');
|
const elCloneAddr = document.querySelector<HTMLInputElement>('#clone_addr');
|
||||||
cloneAddr?.addEventListener('change', () => {
|
const elRepoName = document.querySelector<HTMLInputElement>('#repo_name');
|
||||||
const repoName = document.querySelector<HTMLInputElement>('#repo_name');
|
if (elCloneAddr && elRepoName) {
|
||||||
if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank
|
let repoNameChanged = false;
|
||||||
repoName.value = /^(.*\/)?((.+?)(\.git)?)$/.exec(cloneAddr.value)[3];
|
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() {
|
function checkAuth() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||||
import {htmlEscape} from 'escape-goat';
|
import {htmlEscape} from 'escape-goat';
|
||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
import {sanitizeRepoName} from './repo-common.ts';
|
||||||
|
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
@ -74,6 +75,10 @@ export function initRepoNew() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
inputRepoName.addEventListener('input', updateUiRepoName);
|
inputRepoName.addEventListener('input', updateUiRepoName);
|
||||||
|
inputRepoName.addEventListener('change', () => {
|
||||||
|
inputRepoName.value = sanitizeRepoName(inputRepoName.value);
|
||||||
|
updateUiRepoName();
|
||||||
|
});
|
||||||
updateUiRepoName();
|
updateUiRepoName();
|
||||||
|
|
||||||
initRepoNewTemplateSearch(form);
|
initRepoNewTemplateSearch(form);
|
||||||
|
Reference in New Issue
Block a user