From d2a372fc59c0832f37ac8ef6947fb566f2e40802 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 11 Dec 2025 16:15:40 -0800 Subject: [PATCH 1/9] Move some functions to gitrepo package to reduce RepoPath reference directly (#36126) --- modules/git/diff.go | 14 ------------- modules/git/repo.go | 28 ++++++++++++-------------- modules/git/repo_test.go | 10 --------- modules/gitrepo/clone.go | 4 ++++ modules/gitrepo/commit.go | 16 +++++++++++++++ modules/gitrepo/commit_test.go | 10 +++++++++ modules/gitrepo/diff.go | 14 +++++++++++++ modules/gitrepo/gitrepo.go | 20 ++++++++++++++++++ routers/web/repo/editor_cherry_pick.go | 3 ++- routers/web/repo/issue_comment.go | 4 ++-- services/doctor/misc.go | 25 ++++++++++------------- services/mirror/mirror_pull.go | 8 ++++---- services/pull/check.go | 2 +- services/pull/compare.go | 6 +++--- services/repository/create.go | 7 ++++--- services/repository/files/temp_repo.go | 11 +++++----- services/repository/fork.go | 18 ++++++++--------- services/repository/repository.go | 12 ++++------- 18 files changed, 122 insertions(+), 90 deletions(-) diff --git a/modules/git/diff.go b/modules/git/diff.go index 437b26eb05..c97a2141bf 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -32,20 +32,6 @@ func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer return GetRepoRawDiffForFile(repo, "", commitID, diffType, "", writer) } -// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer. -func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error { - stderr := new(bytes.Buffer) - if err := gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R"). - AddDynamicArguments(commitID). - WithDir(repoPath). - WithStdout(writer). - WithStderr(stderr). - Run(ctx); err != nil { - return fmt.Errorf("Run: %w - %s", err, stderr) - } - return nil -} - // GetRepoRawDiffForFile dumps diff results of file in given commit ID to io.Writer according given repository func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error { commit, err := repo.GetCommit(endCommit) diff --git a/modules/git/repo.go b/modules/git/repo.go index 88acbd30e6..baf29432ec 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -123,6 +123,8 @@ type CloneRepoOptions struct { Depth int Filter string SkipTLSVerify bool + SingleBranch bool + Env []string } // Clone clones original repository to target path. @@ -157,6 +159,9 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { if opts.Filter != "" { cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter) } + if opts.SingleBranch { + cmd.AddArguments("--single-branch") + } if len(opts.Branch) > 0 { cmd.AddArguments("-b").AddDynamicArguments(opts.Branch) } @@ -167,13 +172,17 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { } envs := os.Environ() - u, err := url.Parse(from) - if err == nil { - envs = proxy.EnvWithProxy(u) + if opts.Env != nil { + envs = opts.Env + } else { + u, err := url.Parse(from) + if err == nil { + envs = proxy.EnvWithProxy(u) + } } stderr := new(bytes.Buffer) - if err = cmd. + if err := cmd. WithTimeout(opts.Timeout). WithEnv(envs). WithStdout(io.Discard). @@ -228,14 +237,3 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { return nil } - -// GetLatestCommitTime returns time for latest commit in repository (across all branches) -func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) { - cmd := gitcmd.NewCommand("for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") - stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx) - if err != nil { - return time.Time{}, err - } - commitTime := strings.TrimSpace(stdout) - return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) -} diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go index 26ee3a091a..776c297a34 100644 --- a/modules/git/repo_test.go +++ b/modules/git/repo_test.go @@ -10,16 +10,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetLatestCommitTime(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - lct, err := GetLatestCommitTime(t.Context(), bareRepo1Path) - assert.NoError(t, err) - // Time is Sun Nov 13 16:40:14 2022 +0100 - // which is the time of commit - // ce064814f4a0d337b333e646ece456cd39fab612 (refs/heads/master) - assert.EqualValues(t, 1668354014, lct.Unix()) -} - func TestRepoIsEmpty(t *testing.T) { emptyRepo2Path := filepath.Join(testReposDir, "repo2_empty") repo, err := OpenRepository(t.Context(), emptyRepo2Path) diff --git a/modules/gitrepo/clone.go b/modules/gitrepo/clone.go index 8c437f657c..a0e4cc814c 100644 --- a/modules/gitrepo/clone.go +++ b/modules/gitrepo/clone.go @@ -18,3 +18,7 @@ func CloneExternalRepo(ctx context.Context, fromRemoteURL string, toRepo Reposit func CloneRepoToLocal(ctx context.Context, fromRepo Repository, toLocalPath string, opts git.CloneRepoOptions) error { return git.Clone(ctx, repoPath(fromRepo), toLocalPath, opts) } + +func Clone(ctx context.Context, fromRepo, toRepo Repository, opts git.CloneRepoOptions) error { + return git.Clone(ctx, repoPath(fromRepo), repoPath(toRepo), opts) +} diff --git a/modules/gitrepo/commit.go b/modules/gitrepo/commit.go index e0a87ac10b..da0f3b85a2 100644 --- a/modules/gitrepo/commit.go +++ b/modules/gitrepo/commit.go @@ -7,6 +7,7 @@ import ( "context" "strconv" "strings" + "time" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" @@ -94,3 +95,18 @@ func AllCommitsCount(ctx context.Context, repo Repository, hidePRRefs bool, file return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) } + +func GetFullCommitID(ctx context.Context, repo Repository, shortID string) (string, error) { + return git.GetFullCommitID(ctx, repoPath(repo), shortID) +} + +// GetLatestCommitTime returns time for latest commit in repository (across all branches) +func GetLatestCommitTime(ctx context.Context, repo Repository) (time.Time, error) { + stdout, err := RunCmdString(ctx, repo, + gitcmd.NewCommand("for-each-ref", "--sort=-committerdate", git.BranchPrefix, "--count", "1", "--format=%(committerdate)")) + if err != nil { + return time.Time{}, err + } + commitTime := strings.TrimSpace(stdout) + return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) +} diff --git a/modules/gitrepo/commit_test.go b/modules/gitrepo/commit_test.go index 93483f3e0d..05cedc39ef 100644 --- a/modules/gitrepo/commit_test.go +++ b/modules/gitrepo/commit_test.go @@ -33,3 +33,13 @@ func TestCommitsCountWithoutBase(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(2), commitsCount) } + +func TestGetLatestCommitTime(t *testing.T) { + bareRepo1 := &mockRepository{path: "repo1_bare"} + lct, err := GetLatestCommitTime(t.Context(), bareRepo1) + assert.NoError(t, err) + // Time is Sun Nov 13 16:40:14 2022 +0100 + // which is the time of commit + // ce064814f4a0d337b333e646ece456cd39fab612 (refs/heads/master) + assert.EqualValues(t, 1668354014, lct.Unix()) +} diff --git a/modules/gitrepo/diff.go b/modules/gitrepo/diff.go index c98c3ffcfe..ad7f24762f 100644 --- a/modules/gitrepo/diff.go +++ b/modules/gitrepo/diff.go @@ -4,8 +4,10 @@ package gitrepo import ( + "bytes" "context" "fmt" + "io" "regexp" "strconv" @@ -60,3 +62,15 @@ func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, } return numFiles, totalAdditions, totalDeletions, err } + +// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer. +func GetReverseRawDiff(ctx context.Context, repo Repository, commitID string, writer io.Writer) error { + stderr := new(bytes.Buffer) + if err := RunCmd(ctx, repo, gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R"). + AddDynamicArguments(commitID). + WithStdout(writer). + WithStderr(stderr)); err != nil { + return fmt.Errorf("GetReverseRawDiff: %w - %s", err, stderr) + } + return nil +} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index 4dd03c18fe..c78d2c767d 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -98,3 +98,23 @@ func UpdateServerInfo(ctx context.Context, repo Repository) error { func GetRepoFS(repo Repository) fs.FS { return os.DirFS(repoPath(repo)) } + +func IsRepoFileExist(ctx context.Context, repo Repository, relativeFilePath string) (bool, error) { + absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath) + return util.IsExist(absoluteFilePath) +} + +func IsRepoDirExist(ctx context.Context, repo Repository, relativeDirPath string) (bool, error) { + absoluteDirPath := filepath.Join(repoPath(repo), relativeDirPath) + return util.IsDir(absoluteDirPath) +} + +func RemoveRepoFile(ctx context.Context, repo Repository, relativeFilePath string) error { + absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath) + return util.Remove(absoluteFilePath) +} + +func CreateRepoFile(ctx context.Context, repo Repository, relativeFilePath string) (io.WriteCloser, error) { + absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath) + return os.Create(absoluteFilePath) +} diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go index 32e3c58e87..c1f3ae861b 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -66,7 +67,7 @@ func CherryPickPost(ctx *context.Context) { // Drop through to the "apply" method buf := &bytes.Buffer{} if parsed.form.Revert { - err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf) + err = gitrepo.GetReverseRawDiff(ctx, ctx.Repo.Repository, fromCommitID, buf) } else { err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf) } diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index 35124c5c3e..a3cb88e76a 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -111,7 +111,7 @@ func NewComment(ctx *context.Context) { ctx.ServerError("Unable to load base repo", err) return } - prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef) + prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pull.BaseRepo, prHeadRef) if err != nil { ctx.ServerError("Get head commit Id of pr fail", err) return @@ -128,7 +128,7 @@ func NewComment(ctx *context.Context) { return } headBranchRef := git.RefNameFromBranch(pull.HeadBranch) - headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef.String()) + headBranchCommitID, err := gitrepo.GetFullCommitID(ctx, pull.HeadRepo, headBranchRef.String()) if err != nil { ctx.ServerError("Get head commit Id of head branch fail", err) return diff --git a/services/doctor/misc.go b/services/doctor/misc.go index 445ff61ffb..89f3a63df2 100644 --- a/services/doctor/misc.go +++ b/services/doctor/misc.go @@ -6,9 +6,7 @@ package doctor import ( "context" "fmt" - "os" "os/exec" - "path/filepath" "strings" "code.gitea.io/gitea/models" @@ -20,7 +18,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" lru "github.com/hashicorp/golang-lru/v2" "xorm.io/builder" @@ -142,10 +139,10 @@ func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) err } // Create/Remove git-daemon-export-ok for git-daemon... - daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`) - isExist, err := util.IsExist(daemonExportFile) + daemonExportFile := `git-daemon-export-ok` + isExist, err := gitrepo.IsRepoFileExist(ctx, repo, daemonExportFile) if err != nil { - log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err) + log.Error("Unable to check if %s:%s exists. Error: %v", repo.FullName(), daemonExportFile, err) return err } isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic @@ -154,12 +151,12 @@ func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) err numNeedUpdate++ if autofix { if !isPublic && isExist { - if err = util.Remove(daemonExportFile); err != nil { - log.Error("Failed to remove %s: %v", daemonExportFile, err) + if err = gitrepo.RemoveRepoFile(ctx, repo, daemonExportFile); err != nil { + log.Error("Failed to remove %s:%s: %v", repo.FullName(), daemonExportFile, err) } } else if isPublic && !isExist { - if f, err := os.Create(daemonExportFile); err != nil { - log.Error("Failed to create %s: %v", daemonExportFile, err) + if f, err := gitrepo.CreateRepoFile(ctx, repo, daemonExportFile); err != nil { + log.Error("Failed to create %s:%s: %v", repo.FullName(), daemonExportFile, err) } else { f.Close() } @@ -190,16 +187,16 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro commitGraphExists := func() (bool, error) { // Check commit-graph exists - commitGraphFile := filepath.Join(repo.RepoPath(), `objects/info/commit-graph`) - isExist, err := util.IsExist(commitGraphFile) + commitGraphFile := `objects/info/commit-graph` + isExist, err := gitrepo.IsRepoFileExist(ctx, repo, commitGraphFile) if err != nil { logger.Error("Unable to check if %s exists. Error: %v", commitGraphFile, err) return false, err } if !isExist { - commitGraphsDir := filepath.Join(repo.RepoPath(), `objects/info/commit-graphs`) - isExist, err = util.IsExist(commitGraphsDir) + commitGraphsDir := `objects/info/commit-graphs` + isExist, err = gitrepo.IsRepoDirExist(ctx, repo, commitGraphsDir) if err != nil { logger.Error("Unable to check if %s exists. Error: %v", commitGraphsDir, err) return false, err diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index da58bbd1b6..f9c40049db 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -449,7 +449,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { log.Error("SyncMirrors [repo_id: %v]: unable to GetMirrorByRepoID: %v", repoID, err) return false } - _ = m.GetRepository(ctx) // force load repository of mirror + repo := m.GetRepository(ctx) // force load repository of mirror ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing Mirror %s/%s", m.Repo.OwnerName, m.Repo.Name)) defer finished() @@ -515,12 +515,12 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { } // Push commits - oldCommitID, err := git.GetFullCommitID(gitRepo.Ctx, gitRepo.Path, result.oldCommitID) + oldCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.oldCommitID) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID[%s]: %v", m.Repo, result.oldCommitID, err) continue } - newCommitID, err := git.GetFullCommitID(gitRepo.Ctx, gitRepo.Path, result.newCommitID) + newCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.newCommitID) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID [%s]: %v", m.Repo, result.newCommitID, err) continue @@ -560,7 +560,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool { } if !isEmpty { // Get latest commit date and update to current repository updated time - commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath()) + commitDate, err := gitrepo.GetLatestCommitTime(ctx, m.Repo) if err != nil { log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err) return false diff --git a/services/pull/check.go b/services/pull/check.go index 5b28ec9658..5978a57aec 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -295,7 +295,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com // If merge-base successfully exits then prHeadRef is an ancestor of pr.BaseBranch // Find the head commit id - prHeadCommitID, err := git.GetFullCommitID(ctx, pr.BaseRepo.RepoPath(), prHeadRef) + prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pr.BaseRepo, prHeadRef) if err != nil { return nil, fmt.Errorf("GetFullCommitID(%s) in %s: %w", prHeadRef, pr.BaseRepo.FullName(), err) } diff --git a/services/pull/compare.go b/services/pull/compare.go index 2c4b77a772..c2d39752e8 100644 --- a/services/pull/compare.go +++ b/services/pull/compare.go @@ -48,14 +48,14 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito compareInfo := new(CompareInfo) - compareInfo.HeadCommitID, err = git.GetFullCommitID(ctx, headGitRepo.Path, headBranch) + compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headBranch) if err != nil { compareInfo.HeadCommitID = headBranch } compareInfo.MergeBase, remoteBranch, err = headGitRepo.GetMergeBase(tmpRemote, baseBranch, headBranch) if err == nil { - compareInfo.BaseCommitID, err = git.GetFullCommitID(ctx, headGitRepo.Path, remoteBranch) + compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, remoteBranch) if err != nil { compareInfo.BaseCommitID = remoteBranch } @@ -77,7 +77,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito } } else { compareInfo.Commits = []*git.Commit{} - compareInfo.MergeBase, err = git.GetFullCommitID(ctx, headGitRepo.Path, remoteBranch) + compareInfo.MergeBase, err = gitrepo.GetFullCommitID(ctx, headRepo, remoteBranch) if err != nil { compareInfo.MergeBase = remoteBranch } diff --git a/services/repository/create.go b/services/repository/create.go index 0b57db988b..7439fc8f08 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -69,9 +69,10 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir ) // Clone to temporary path and do the init commit. - if stdout, _, err := gitcmd.NewCommand("clone").AddDynamicArguments(repo.RepoPath(), tmpDir). - WithEnv(env).RunStdString(ctx); err != nil { - log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) + if err := gitrepo.CloneRepoToLocal(ctx, repo, tmpDir, git.CloneRepoOptions{ + Env: env, + }); err != nil { + log.Error("Failed to clone from %v into %s\nError: %v", repo, tmpDir, err) return fmt.Errorf("git clone: %w", err) } diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index b7f4afdebc..aaf9566aec 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -55,12 +55,11 @@ func (t *TemporaryUploadRepository) Close() { // Clone the base repository to our path and set branch as the HEAD func (t *TemporaryUploadRepository) Clone(ctx context.Context, branch string, bare bool) error { - cmd := gitcmd.NewCommand("clone", "-s", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath) - if bare { - cmd.AddArguments("--bare") - } - - if _, _, err := cmd.RunStdString(ctx); err != nil { + if err := gitrepo.CloneRepoToLocal(ctx, t.repo, t.basePath, git.CloneRepoOptions{ + Bare: bare, + Branch: branch, + Shared: true, + }); err != nil { stderr := err.Error() if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched { return git.ErrBranchNotExist{ diff --git a/services/repository/fork.go b/services/repository/fork.go index 2380666afb..f92af65605 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -15,7 +15,6 @@ import ( "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/git/gitcmd" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" @@ -147,15 +146,16 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork } // 3 - Clone the repository - cloneCmd := gitcmd.NewCommand("clone", "--bare") - if opts.SingleBranch != "" { - cloneCmd.AddArguments("--single-branch", "--branch").AddDynamicArguments(opts.SingleBranch) + cloneOpts := git.CloneRepoOptions{ + Bare: true, + Timeout: 10 * time.Minute, } - var stdout []byte - if stdout, _, err = cloneCmd.AddDynamicArguments(opts.BaseRepo.RepoPath(), repo.RepoPath()). - WithTimeout(10 * time.Minute). - RunStdBytes(ctx); err != nil { - log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err) + if opts.SingleBranch != "" { + cloneOpts.SingleBranch = true + cloneOpts.Branch = opts.SingleBranch + } + if err = gitrepo.Clone(ctx, opts.BaseRepo, repo, cloneOpts); err != nil { + log.Error("Fork Repository (git clone) Failed for %v (from %v):\nError: %v", repo, opts.BaseRepo, err) return nil, fmt.Errorf("git clone: %w", err) } diff --git a/services/repository/repository.go b/services/repository/repository.go index 93fbcb51f7..a4d82140c6 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -7,8 +7,6 @@ import ( "context" "errors" "fmt" - "os" - "path/filepath" "strings" activities_model "code.gitea.io/gitea/models/activities" @@ -28,7 +26,6 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" pull_service "code.gitea.io/gitea/services/pull" ) @@ -251,9 +248,8 @@ func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error } // Create/Remove git-daemon-export-ok for git-daemon... - daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`) - - isExist, err := util.IsExist(daemonExportFile) + daemonExportFile := `git-daemon-export-ok` + isExist, err := gitrepo.IsRepoFileExist(ctx, repo, daemonExportFile) if err != nil { log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err) return err @@ -261,11 +257,11 @@ func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic if !isPublic && isExist { - if err = util.Remove(daemonExportFile); err != nil { + if err = gitrepo.RemoveRepoFile(ctx, repo, daemonExportFile); err != nil { log.Error("Failed to remove %s: %v", daemonExportFile, err) } } else if isPublic && !isExist { - if f, err := os.Create(daemonExportFile); err != nil { + if f, err := gitrepo.CreateRepoFile(ctx, repo, daemonExportFile); err != nil { log.Error("Failed to create %s: %v", daemonExportFile, err) } else { f.Close() From bfbc38f40c1cbab851040873e921ebb1306df6b3 Mon Sep 17 00:00:00 2001 From: junoberryferry Date: Thu, 11 Dec 2025 23:12:06 -0500 Subject: [PATCH 2/9] Add sorting/filtering to admin user search API endpoint (#36112) --- models/user/search.go | 17 +++++ routers/api/v1/admin/user.go | 110 ++++++++++++++++++++++++++++++--- templates/swagger/v1_json.tmpl | 57 +++++++++++++++++ 3 files changed, 176 insertions(+), 8 deletions(-) diff --git a/models/user/search.go b/models/user/search.go index db4b07f64a..36d1d3913b 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -18,6 +18,23 @@ import ( "xorm.io/xorm" ) +// AdminUserOrderByMap represents all possible admin user search orders +// This should only be used for admin API endpoints as we should not expose "updated" ordering which could expose recent user activity including logins. +var AdminUserOrderByMap = map[string]map[string]db.SearchOrderBy{ + "asc": { + "name": db.SearchOrderByAlphabetically, + "created": db.SearchOrderByOldest, + "updated": db.SearchOrderByLeastUpdated, + "id": db.SearchOrderByID, + }, + "desc": { + "name": db.SearchOrderByAlphabeticallyReverse, + "created": db.SearchOrderByNewest, + "updated": db.SearchOrderByRecentUpdated, + "id": db.SearchOrderByIDReverse, + }, +} + // SearchUserOptions contains the options for searching type SearchUserOptions struct { db.ListOptions diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 6f1e2eb120..6bed410642 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -414,22 +414,116 @@ func SearchUsers(ctx *context.APIContext) { // in: query // description: page size of results // type: integer + // - name: sort + // in: query + // description: sort users by attribute. Supported values are + // "name", "created", "updated" and "id". + // Default is "name" + // type: string + // - name: order + // in: query + // description: sort order, either "asc" (ascending) or "desc" (descending). + // Default is "asc", ignored if "sort" is not specified. + // type: string + // - name: q + // in: query + // description: search term (username, full name, email) + // type: string + // - name: visibility + // in: query + // description: visibility filter. Supported values are + // "public", "limited" and "private". + // type: string + // - name: is_active + // in: query + // description: filter active users + // type: boolean + // - name: is_admin + // in: query + // description: filter admin users + // type: boolean + // - name: is_restricted + // in: query + // description: filter restricted users + // type: boolean + // - name: is_2fa_enabled + // in: query + // description: filter 2FA enabled users + // type: boolean + // - name: is_prohibit_login + // in: query + // description: filter login prohibited users + // type: boolean // responses: // "200": // "$ref": "#/responses/UserList" // "403": // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" listOptions := utils.GetListOptions(ctx) - users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ - Actor: ctx.Doer, - Types: []user_model.UserType{user_model.UserTypeIndividual}, - LoginName: ctx.FormTrim("login_name"), - SourceID: ctx.FormInt64("source_id"), - OrderBy: db.SearchOrderByAlphabetically, - ListOptions: listOptions, - }) + orderBy := db.SearchOrderByAlphabetically + sortMode := ctx.FormString("sort") + if len(sortMode) > 0 { + sortOrder := ctx.FormString("order") + if len(sortOrder) == 0 { + sortOrder = "asc" + } + if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok { + if order, ok := searchModeMap[sortMode]; ok { + orderBy = order + } else { + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode)) + return + } + } else { + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder)) + return + } + } + + var visible []api.VisibleType + visibilityParam := ctx.FormString("visibility") + if len(visibilityParam) > 0 { + if visibility, ok := api.VisibilityModes[visibilityParam]; ok { + visible = []api.VisibleType{visibility} + } else { + ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam)) + return + } + } + + searchOpts := user_model.SearchUserOptions{ + Actor: ctx.Doer, + Types: []user_model.UserType{user_model.UserTypeIndividual}, + LoginName: ctx.FormTrim("login_name"), + SourceID: ctx.FormInt64("source_id"), + Keyword: ctx.FormTrim("q"), + Visible: visible, + OrderBy: orderBy, + ListOptions: listOptions, + SearchByEmail: true, + } + + if ctx.FormString("is_active") != "" { + searchOpts.IsActive = optional.Some(ctx.FormBool("is_active")) + } + if ctx.FormString("is_admin") != "" { + searchOpts.IsAdmin = optional.Some(ctx.FormBool("is_admin")) + } + if ctx.FormString("is_restricted") != "" { + searchOpts.IsRestricted = optional.Some(ctx.FormBool("is_restricted")) + } + if ctx.FormString("is_2fa_enabled") != "" { + searchOpts.IsTwoFactorEnabled = optional.Some(ctx.FormBool("is_2fa_enabled")) + } + if ctx.FormString("is_prohibit_login") != "" { + searchOpts.IsProhibitLogin = optional.Some(ctx.FormBool("is_prohibit_login")) + } + + users, maxResults, err := user_model.SearchUsers(ctx, searchOpts) if err != nil { ctx.APIErrorInternal(err) return diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index b37937dcee..056e05ae4d 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -781,6 +781,60 @@ "description": "page size of results", "name": "limit", "in": "query" + }, + { + "type": "string", + "description": "sort users by attribute. Supported values are \"name\", \"created\", \"updated\" and \"id\". Default is \"name\"", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "sort order, either \"asc\" (ascending) or \"desc\" (descending). Default is \"asc\", ignored if \"sort\" is not specified.", + "name": "order", + "in": "query" + }, + { + "type": "string", + "description": "search term (username, full name, email)", + "name": "q", + "in": "query" + }, + { + "type": "string", + "description": "visibility filter. Supported values are \"public\", \"limited\" and \"private\".", + "name": "visibility", + "in": "query" + }, + { + "type": "boolean", + "description": "filter active users", + "name": "is_active", + "in": "query" + }, + { + "type": "boolean", + "description": "filter admin users", + "name": "is_admin", + "in": "query" + }, + { + "type": "boolean", + "description": "filter restricted users", + "name": "is_restricted", + "in": "query" + }, + { + "type": "boolean", + "description": "filter 2FA enabled users", + "name": "is_2fa_enabled", + "in": "query" + }, + { + "type": "boolean", + "description": "filter login prohibited users", + "name": "is_prohibit_login", + "in": "query" } ], "responses": { @@ -789,6 +843,9 @@ }, "403": { "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" } } }, From 4cbcb91b7bc6ac724b96ad5682be80bce4efc2b3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 Dec 2025 08:39:02 +0100 Subject: [PATCH 3/9] Fix SVG size calulation, only use `style` attribute (#36133) Fixes: https://github.com/go-gitea/gitea/issues/35863 The old code had a conflict between using HTML attributes vs. style properties where the style was overriding the previously set HTML attributes: ```html ``` I made it so in all cases only `style` properties are used and the previous width/height values are now set via `style`. Also I did a number of much-needed typescript improvements to the file. --------- Co-authored-by: wxiaoguang --- web_src/css/base.css | 2 + web_src/css/features/imagediff.css | 2 +- web_src/css/repo/file-view.css | 1 + web_src/js/features/imagediff.ts | 198 ++++++++++++++++------------- 4 files changed, 115 insertions(+), 88 deletions(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index be28cd6fea..0e690a0265 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -39,6 +39,8 @@ --gap-inline: 0.25rem; /* gap for inline texts and elements, for example: the spaces for sentence with labels, button text, etc */ --gap-block: 0.5rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */ + + --background-view-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAG0lEQVQYlWN4+vTpf3SMDTAMBYXYBLFpHgoKAeiOf0SGE9kbAAAAAElFTkSuQmCC") right bottom var(--color-primary-light-7); } @media (min-width: 768px) and (max-width: 1200px) { diff --git a/web_src/css/features/imagediff.css b/web_src/css/features/imagediff.css index ad3165e8d8..d32a2098ca 100644 --- a/web_src/css/features/imagediff.css +++ b/web_src/css/features/imagediff.css @@ -13,7 +13,7 @@ .image-diff-container img { border: 1px solid var(--color-primary-light-7); - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAG0lEQVQYlWN4+vTpf3SMDTAMBYXYBLFpHgoKAeiOf0SGE9kbAAAAAElFTkSuQmCC") right bottom var(--color-primary-light-7); + background: var(--background-view-image); } .image-diff-container .before-container { diff --git a/web_src/css/repo/file-view.css b/web_src/css/repo/file-view.css index 907f136afe..3f1c42a4a1 100644 --- a/web_src/css/repo/file-view.css +++ b/web_src/css/repo/file-view.css @@ -81,6 +81,7 @@ .view-raw img[src$=".svg" i] { max-height: 600px !important; max-width: 600px !important; + background: var(--background-view-image); } .file-view-render-container { diff --git a/web_src/js/features/imagediff.ts b/web_src/js/features/imagediff.ts index 4ace1ca2ad..23f05fbdc7 100644 --- a/web_src/js/features/imagediff.ts +++ b/web_src/js/features/imagediff.ts @@ -3,7 +3,33 @@ import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts import {parseDom} from '../utils.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; -function getDefaultSvgBoundsIfUndefined(text: string, src: string) { +type ImageContext = { + imageBefore: HTMLImageElement | undefined, + imageAfter: HTMLImageElement | undefined, + sizeBefore: {width: number, height: number}, + sizeAfter: {width: number, height: number}, + maxSize: {width: number, height: number}, + ratio: [number, number, number, number], +}; + +type ImageInfo = { + path: string | null, + mime: string | null, + images: NodeListOf, + boundsInfo: HTMLElement | null, +}; + +type Bounds = { + width: number, + height: number, +} | null; + +type SvgBoundsInfo = { + before: Bounds, + after: Bounds, +}; + +function getDefaultSvgBoundsIfUndefined(text: string, src: string): Bounds | null { const defaultSize = 300; const maxSize = 99999; @@ -38,14 +64,14 @@ function getDefaultSvgBoundsIfUndefined(text: string, src: string) { return null; } -function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement) { +function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement, svgBoundsInfo: SvgBoundsInfo): ImageContext { const sizeAfter = { - width: imageAfter?.width || 0, - height: imageAfter?.height || 0, + width: svgBoundsInfo.after?.width || imageAfter?.width || 0, + height: svgBoundsInfo.after?.height || imageAfter?.height || 0, }; const sizeBefore = { - width: imageBefore?.width || 0, - height: imageBefore?.height || 0, + width: svgBoundsInfo.before?.width || imageBefore?.width || 0, + height: svgBoundsInfo.before?.height || imageBefore?.height || 0, }; const maxSize = { width: Math.max(sizeBefore.width, sizeAfter.width), @@ -80,7 +106,7 @@ class ImageDiff { // the container may be hidden by "viewed" checkbox, so use the parent's width for reference this.diffContainerWidth = Math.max(containerEl.closest('.diff-file-box')!.clientWidth - 300, 100); - const imageInfos = [{ + const imagePair: [ImageInfo, ImageInfo] = [{ path: containerEl.getAttribute('data-path-after'), mime: containerEl.getAttribute('data-mime-after'), images: containerEl.querySelectorAll('img.image-after'), // matches 3 @@ -92,7 +118,8 @@ class ImageDiff { boundsInfo: containerEl.querySelector('.bounds-info-before'), }]; - await Promise.all(imageInfos.map(async (info) => { + const svgBoundsInfo: SvgBoundsInfo = {before: null, after: null}; + await Promise.all(imagePair.map(async (info, index) => { const [success] = await Promise.all(Array.from(info.images, (img) => { return loadElem(img, info.path!); })); @@ -102,115 +129,112 @@ class ImageDiff { const resp = await GET(info.path!); const text = await resp.text(); const bounds = getDefaultSvgBoundsIfUndefined(text, info.path!); + svgBoundsInfo[index === 0 ? 'after' : 'before'] = bounds; if (bounds) { - for (const el of info.images) { - el.setAttribute('width', String(bounds.width)); - el.setAttribute('height', String(bounds.height)); - } hideElem(info.boundsInfo!); } } })); - const imagesAfter = imageInfos[0].images; - const imagesBefore = imageInfos[1].images; + const imagesAfter = imagePair[0].images; + const imagesBefore = imagePair[1].images; - this.initSideBySide(createContext(imagesAfter[0], imagesBefore[0])); + this.initSideBySide(createContext(imagesAfter[0], imagesBefore[0], svgBoundsInfo)); if (imagesAfter.length > 0 && imagesBefore.length > 0) { - this.initSwipe(createContext(imagesAfter[1], imagesBefore[1])); - this.initOverlay(createContext(imagesAfter[2], imagesBefore[2])); + this.initSwipe(createContext(imagesAfter[1], imagesBefore[1], svgBoundsInfo)); + this.initOverlay(createContext(imagesAfter[2], imagesBefore[2], svgBoundsInfo)); } queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); } - initSideBySide(sizes: Record) { + initSideBySide(ctx: ImageContext) { let factor = 1; - if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) { - factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width; + if (ctx.maxSize.width > (this.diffContainerWidth - 24) / 2) { + factor = (this.diffContainerWidth - 24) / 2 / ctx.maxSize.width; } - const widthChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalWidth !== sizes.imageBefore.naturalWidth; - const heightChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalHeight !== sizes.imageBefore.naturalHeight; - if (sizes.imageAfter) { + const widthChanged = ctx.imageAfter && ctx.imageBefore && ctx.imageAfter.naturalWidth !== ctx.imageBefore.naturalWidth; + const heightChanged = ctx.imageAfter && ctx.imageBefore && ctx.imageAfter.naturalHeight !== ctx.imageBefore.naturalHeight; + if (ctx.imageAfter) { const boundsInfoAfterWidth = this.containerEl.querySelector('.bounds-info-after .bounds-info-width'); if (boundsInfoAfterWidth) { - boundsInfoAfterWidth.textContent = `${sizes.imageAfter.naturalWidth}px`; + boundsInfoAfterWidth.textContent = `${ctx.imageAfter.naturalWidth}px`; boundsInfoAfterWidth.classList.toggle('green', widthChanged); } const boundsInfoAfterHeight = this.containerEl.querySelector('.bounds-info-after .bounds-info-height'); if (boundsInfoAfterHeight) { - boundsInfoAfterHeight.textContent = `${sizes.imageAfter.naturalHeight}px`; + boundsInfoAfterHeight.textContent = `${ctx.imageAfter.naturalHeight}px`; boundsInfoAfterHeight.classList.toggle('green', heightChanged); } } - if (sizes.imageBefore) { + if (ctx.imageBefore) { const boundsInfoBeforeWidth = this.containerEl.querySelector('.bounds-info-before .bounds-info-width'); if (boundsInfoBeforeWidth) { - boundsInfoBeforeWidth.textContent = `${sizes.imageBefore.naturalWidth}px`; + boundsInfoBeforeWidth.textContent = `${ctx.imageBefore.naturalWidth}px`; boundsInfoBeforeWidth.classList.toggle('red', widthChanged); } const boundsInfoBeforeHeight = this.containerEl.querySelector('.bounds-info-before .bounds-info-height'); if (boundsInfoBeforeHeight) { - boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`; + boundsInfoBeforeHeight.textContent = `${ctx.imageBefore.naturalHeight}px`; boundsInfoBeforeHeight.classList.toggle('red', heightChanged); } } - if (sizes.imageAfter) { - const container = sizes.imageAfter.parentNode; - sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; - sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; + if (ctx.imageAfter) { + const container = ctx.imageAfter.parentNode as HTMLElement; + ctx.imageAfter.style.width = `${ctx.sizeAfter.width * factor}px`; + ctx.imageAfter.style.height = `${ctx.sizeAfter.height * factor}px`; container.style.margin = '10px auto'; - container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; - container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; + container.style.width = `${ctx.sizeAfter.width * factor + 2}px`; + container.style.height = `${ctx.sizeAfter.height * factor + 2}px`; } - if (sizes.imageBefore) { - const container = sizes.imageBefore.parentNode; - sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; - sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; + if (ctx.imageBefore) { + const container = ctx.imageBefore.parentNode as HTMLElement; + ctx.imageBefore.style.width = `${ctx.sizeBefore.width * factor}px`; + ctx.imageBefore.style.height = `${ctx.sizeBefore.height * factor}px`; container.style.margin = '10px auto'; - container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; - container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; + container.style.width = `${ctx.sizeBefore.width * factor + 2}px`; + container.style.height = `${ctx.sizeBefore.height * factor + 2}px`; } } - initSwipe(sizes: Record) { + initSwipe(ctx: ImageContext) { let factor = 1; - if (sizes.maxSize.width > this.diffContainerWidth - 12) { - factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; + if (ctx.maxSize.width > this.diffContainerWidth - 12) { + factor = (this.diffContainerWidth - 12) / ctx.maxSize.width; } - if (sizes.imageAfter) { - const imgParent = sizes.imageAfter.parentNode; - const swipeFrame = imgParent.parentNode; - sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; - sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; - imgParent.style.margin = `0px ${sizes.ratio[0] * factor}px`; - imgParent.style.width = `${sizes.sizeAfter.width * factor + 2}px`; - imgParent.style.height = `${sizes.sizeAfter.height * factor + 2}px`; - swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`; - swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; + if (ctx.imageAfter) { + const imgParent = ctx.imageAfter.parentNode as HTMLElement; + const swipeFrame = imgParent.parentNode as HTMLElement; + ctx.imageAfter.style.width = `${ctx.sizeAfter.width * factor}px`; + ctx.imageAfter.style.height = `${ctx.sizeAfter.height * factor}px`; + imgParent.style.margin = `0px ${ctx.ratio[0] * factor}px`; + imgParent.style.width = `${ctx.sizeAfter.width * factor + 2}px`; + imgParent.style.height = `${ctx.sizeAfter.height * factor + 2}px`; + swipeFrame.style.padding = `${ctx.ratio[1] * factor}px 0 0 0`; + swipeFrame.style.width = `${ctx.maxSize.width * factor + 2}px`; } - if (sizes.imageBefore) { - const imgParent = sizes.imageBefore.parentNode; - const swipeFrame = imgParent.parentNode; - sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; - sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; - imgParent.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; - imgParent.style.width = `${sizes.sizeBefore.width * factor + 2}px`; - imgParent.style.height = `${sizes.sizeBefore.height * factor + 2}px`; - swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; - swipeFrame.style.height = `${sizes.maxSize.height * factor + 2}px`; + if (ctx.imageBefore) { + const imgParent = ctx.imageBefore.parentNode as HTMLElement; + const swipeFrame = imgParent.parentNode as HTMLElement; + ctx.imageBefore.style.width = `${ctx.sizeBefore.width * factor}px`; + ctx.imageBefore.style.height = `${ctx.sizeBefore.height * factor}px`; + imgParent.style.margin = `${ctx.ratio[3] * factor}px ${ctx.ratio[2] * factor}px`; + imgParent.style.width = `${ctx.sizeBefore.width * factor + 2}px`; + imgParent.style.height = `${ctx.sizeBefore.height * factor + 2}px`; + swipeFrame.style.width = `${ctx.maxSize.width * factor + 2}px`; + swipeFrame.style.height = `${ctx.maxSize.height * factor + 2}px`; } // extra height for inner "position: absolute" elements const swipe = this.containerEl.querySelector('.diff-swipe'); if (swipe) { - swipe.style.width = `${sizes.maxSize.width * factor + 2}px`; - swipe.style.height = `${sizes.maxSize.height * factor + 30}px`; + swipe.style.width = `${ctx.maxSize.width * factor + 2}px`; + swipe.style.height = `${ctx.maxSize.height * factor + 30}px`; } this.containerEl.querySelector('.swipe-bar')!.addEventListener('mousedown', (e) => { @@ -237,40 +261,40 @@ class ImageDiff { document.addEventListener('mouseup', removeEventListeners); } - initOverlay(sizes: Record) { + initOverlay(ctx: ImageContext) { let factor = 1; - if (sizes.maxSize.width > this.diffContainerWidth - 12) { - factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; + if (ctx.maxSize.width > this.diffContainerWidth - 12) { + factor = (this.diffContainerWidth - 12) / ctx.maxSize.width; } - if (sizes.imageAfter) { - const container = sizes.imageAfter.parentNode; - sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`; - sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`; - container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`; - container.style.width = `${sizes.sizeAfter.width * factor + 2}px`; - container.style.height = `${sizes.sizeAfter.height * factor + 2}px`; + if (ctx.imageAfter) { + const container = ctx.imageAfter.parentNode as HTMLElement; + ctx.imageAfter.style.width = `${ctx.sizeAfter.width * factor}px`; + ctx.imageAfter.style.height = `${ctx.sizeAfter.height * factor}px`; + container.style.margin = `${ctx.ratio[1] * factor}px ${ctx.ratio[0] * factor}px`; + container.style.width = `${ctx.sizeAfter.width * factor + 2}px`; + container.style.height = `${ctx.sizeAfter.height * factor + 2}px`; } - if (sizes.imageBefore) { - const container = sizes.imageBefore.parentNode; - const overlayFrame = container.parentNode; - sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`; - sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`; - container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; - container.style.width = `${sizes.sizeBefore.width * factor + 2}px`; - container.style.height = `${sizes.sizeBefore.height * factor + 2}px`; + if (ctx.imageBefore) { + const container = ctx.imageBefore.parentNode as HTMLElement; + const overlayFrame = container.parentNode as HTMLElement; + ctx.imageBefore.style.width = `${ctx.sizeBefore.width * factor}px`; + ctx.imageBefore.style.height = `${ctx.sizeBefore.height * factor}px`; + container.style.margin = `${ctx.ratio[3] * factor}px ${ctx.ratio[2] * factor}px`; + container.style.width = `${ctx.sizeBefore.width * factor + 2}px`; + container.style.height = `${ctx.sizeBefore.height * factor + 2}px`; // some inner elements are `position: absolute`, so the container's height must be large enough - overlayFrame.style.width = `${sizes.maxSize.width * factor + 2}px`; - overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`; + overlayFrame.style.width = `${ctx.maxSize.width * factor + 2}px`; + overlayFrame.style.height = `${ctx.maxSize.height * factor + 2}px`; } const rangeInput = this.containerEl.querySelector('input[type="range"]')!; function updateOpacity() { - if (sizes.imageAfter) { - sizes.imageAfter.parentNode.style.opacity = `${Number(rangeInput.value) / 100}`; + if (ctx.imageAfter) { + (ctx.imageAfter.parentNode as HTMLElement).style.opacity = `${Number(rangeInput.value) / 100}`; } } From 906adff0c1223c6cde37df34e35f2310a15ef6f8 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 12 Dec 2025 01:26:15 -0800 Subject: [PATCH 4/9] Hide RSS icon when viewing a file not under a branch (#36135) Fix #35855 Co-authored-by: Giteabot --- templates/repo/view_file.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 8fce1b6f2c..809b1e9677 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -62,7 +62,7 @@ {{if not .IsDisplayingSource}}data-raw-file-link="{{$.RawFileLink}}"{{end}} data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}" >{{svg "octicon-copy"}} - {{if .EnableFeed}} + {{if and .EnableFeed .RefFullName.IsBranch}} {{svg "octicon-rss"}} From 87b855bd15336c1d7029a18ee5ce87d8841b6abe Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 Dec 2025 16:44:53 +0100 Subject: [PATCH 5/9] Bump `actions/checkout` to v6 (#36136) https://github.com/actions/checkout#checkout-v6 Result of `perl -p -i -e 's#actions\/checkout\@v5#actions/checkout\@v6#g' .github/workflows/*` --- .github/workflows/cron-licenses.yml | 2 +- .github/workflows/cron-translations.yml | 2 +- .github/workflows/files-changed.yml | 2 +- .github/workflows/pull-compliance.yml | 24 +++++++++++------------ .github/workflows/pull-db-tests.yml | 10 +++++----- .github/workflows/pull-docker-dryrun.yml | 2 +- .github/workflows/release-nightly.yml | 4 ++-- .github/workflows/release-tag-rc.yml | 4 ++-- .github/workflows/release-tag-version.yml | 4 ++-- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index 12f52289b6..5b34d5c8ec 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index ae2238ad2d..334a221893 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: crowdin/github-action@v1 with: upload_sources: true diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index b21341a277..e0c2870319 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -34,7 +34,7 @@ jobs: swagger: ${{ steps.changes.outputs.swagger }} yaml: ${{ steps.changes.outputs.yaml }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: dorny/paths-filter@v3 id: changes with: diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index f73772e934..065bdb26db 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -16,7 +16,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -31,7 +31,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v6 - run: uv python install 3.12 - uses: pnpm/action-setup@v4 @@ -47,7 +47,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v6 - run: uv python install 3.12 - run: make deps-py @@ -58,7 +58,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -71,7 +71,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -83,7 +83,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -100,7 +100,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -115,7 +115,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -128,7 +128,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -144,7 +144,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -176,7 +176,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v5 with: @@ -189,7 +189,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 21ec76b48e..1d5a652d6f 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -38,7 +38,7 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -66,7 +66,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -124,7 +124,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -177,7 +177,7 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -217,7 +217,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index 9c9dd2ffe6..2b4b2b49be 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -16,7 +16,7 @@ jobs: needs: files-changed runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: docker/setup-buildx-action@v3 - name: Build regular container image uses: docker/build-push-action@v5 diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index ada4c18d33..3e0dab9edf 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -12,7 +12,7 @@ jobs: nightly-binary: runs-on: namespace-profile-gitea-release-binary steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -61,7 +61,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 35558933e0..eb43063291 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -13,7 +13,7 @@ jobs: binary: runs-on: namespace-profile-gitea-release-binary steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -71,7 +71,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 56426d3bc3..4ade365d9c 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -17,7 +17,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -75,7 +75,7 @@ jobs: permissions: packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force From 4c06c98dda6638f9f386ee5f13d0513cabe0470f Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 Dec 2025 17:48:29 +0100 Subject: [PATCH 6/9] Add explicit permissions to all actions workflows (#36140) Explicitely specify all workflow [`permissions`](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions). This will fix [26 CodeQL alerts](https://github.com/go-gitea/gitea/security/code-scanning?query=permissions+is%3Aopen+branch%3Amain+). --- .github/workflows/cron-licenses.yml | 2 ++ .github/workflows/cron-translations.yml | 2 ++ .github/workflows/files-changed.yml | 2 ++ .github/workflows/pull-compliance.yml | 24 +++++++++++++++++++++++ .github/workflows/pull-db-tests.yml | 10 ++++++++++ .github/workflows/pull-docker-dryrun.yml | 2 ++ .github/workflows/release-nightly.yml | 4 ++++ .github/workflows/release-tag-rc.yml | 4 ++++ .github/workflows/release-tag-version.yml | 3 +++ 9 files changed, 53 insertions(+) diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml index 5b34d5c8ec..a8be1ffa59 100644 --- a/.github/workflows/cron-licenses.yml +++ b/.github/workflows/cron-licenses.yml @@ -9,6 +9,8 @@ jobs: cron-licenses: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' + permissions: + contents: write steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 diff --git a/.github/workflows/cron-translations.yml b/.github/workflows/cron-translations.yml index 334a221893..3a012e9876 100644 --- a/.github/workflows/cron-translations.yml +++ b/.github/workflows/cron-translations.yml @@ -9,6 +9,8 @@ jobs: crowdin-pull: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' + permissions: + contents: write steps: - uses: actions/checkout@v6 - uses: crowdin/github-action@v1 diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index e0c2870319..d18ee6e998 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -24,6 +24,8 @@ jobs: detect: runs-on: ubuntu-latest timeout-minutes: 3 + permissions: + contents: read outputs: backend: ${{ steps.changes.outputs.backend }} frontend: ${{ steps.changes.outputs.frontend }} diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 065bdb26db..9e1963d48a 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -15,6 +15,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -30,6 +32,8 @@ jobs: if: needs.files-changed.outputs.templates == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v6 @@ -46,6 +50,8 @@ jobs: if: needs.files-changed.outputs.yaml == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v6 @@ -57,6 +63,8 @@ jobs: if: needs.files-changed.outputs.swagger == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 @@ -70,6 +78,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -82,6 +92,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -99,6 +111,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -114,6 +128,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -127,6 +143,8 @@ jobs: if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 @@ -143,6 +161,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -175,6 +195,8 @@ jobs: if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 @@ -188,6 +210,8 @@ jobs: if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 1d5a652d6f..16c9e004a5 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -15,6 +15,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: pgsql: image: postgres:14 @@ -65,6 +67,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 @@ -90,6 +94,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: elasticsearch: image: elasticsearch:7.5.0 @@ -152,6 +158,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: mysql: # the bitnami mysql image has more options than the official one, it's easier to customize @@ -203,6 +211,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: mssql: image: mcr.microsoft.com/mssql/server:2019-latest diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index 2b4b2b49be..e1b86e5e38 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -15,6 +15,8 @@ jobs: if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v6 - uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 3e0dab9edf..c8ce0aa787 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -11,6 +11,8 @@ concurrency: jobs: nightly-binary: runs-on: namespace-profile-gitea-release-binary + permissions: + contents: read steps: - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -56,9 +58,11 @@ jobs: - name: upload binaries to s3 run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress + nightly-container: runs-on: namespace-profile-gitea-release-docker permissions: + contents: read packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index eb43063291..ef36e55a94 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -12,6 +12,8 @@ concurrency: jobs: binary: runs-on: namespace-profile-gitea-release-binary + permissions: + contents: read steps: - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -66,9 +68,11 @@ jobs: gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/* env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + container: runs-on: namespace-profile-gitea-release-docker permissions: + contents: read packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 4ade365d9c..a3838de3c0 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -15,6 +15,7 @@ jobs: binary: runs-on: namespace-profile-gitea-release-binary permissions: + contents: read packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v6 @@ -70,9 +71,11 @@ jobs: gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/* env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + container: runs-on: namespace-profile-gitea-release-docker permissions: + contents: read packages: write # to publish to ghcr.io steps: - uses: actions/checkout@v6 From 3e57ba5b36a110065804a3f70f63b10587b17ea3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 Dec 2025 18:38:59 +0100 Subject: [PATCH 7/9] Add permissions to`files-changed` jobs (#36142) Followup to https://github.com/go-gitea/gitea/pull/36140. `files-changed` is a job that imports another workflow via `uses` statement but CodeQL still complains about lack of permissions on these jobs, so add it. This will fix the remaining [3 CodeQL issues](https://github.com/go-gitea/gitea/security/code-scanning?query=is%3Aopen+branch%3Amain+permissions). --- .github/workflows/pull-compliance.yml | 2 ++ .github/workflows/pull-db-tests.yml | 2 ++ .github/workflows/pull-docker-dryrun.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 9e1963d48a..c146b439e0 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -10,6 +10,8 @@ concurrency: jobs: files-changed: uses: ./.github/workflows/files-changed.yml + permissions: + contents: read lint-backend: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 16c9e004a5..66f48d5af8 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -10,6 +10,8 @@ concurrency: jobs: files-changed: uses: ./.github/workflows/files-changed.yml + permissions: + contents: read test-pgsql: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' diff --git a/.github/workflows/pull-docker-dryrun.yml b/.github/workflows/pull-docker-dryrun.yml index e1b86e5e38..1cd1ba31dd 100644 --- a/.github/workflows/pull-docker-dryrun.yml +++ b/.github/workflows/pull-docker-dryrun.yml @@ -10,6 +10,8 @@ concurrency: jobs: files-changed: uses: ./.github/workflows/files-changed.yml + permissions: + contents: read container: if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true' From 3102c04c1eb9251d933797465e4187d60b17e8a0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 12 Dec 2025 19:12:35 +0100 Subject: [PATCH 8/9] Fix issue close timeline icon (#36138) Previously there was a icon mismatch between a issue's label and the timeline close event icon --- templates/repo/issue/view_content/comments.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 089cdf2ccd..6d23186d08 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -96,7 +96,7 @@ {{else if eq .Type 2}}
- {{svg "octicon-circle-slash"}} + {{svg "octicon-issue-closed"}} {{if not .OriginalAuthor}} {{template "shared/user/avatarlink" dict "user" .Poster}} {{end}} From 1e72b1563906ef5625f7f0dcb67ed4bad5e2429c Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 13 Dec 2025 02:56:05 +0800 Subject: [PATCH 9/9] Fix various bugs (#36139) * Fix #35768 * Fix #36064 * Fix #36051 * Fix cherry-pick panic --- modules/packages/npm/creator.go | 25 +++++++++++++++++++-- modules/packages/npm/creator_test.go | 28 ++++++++++++++++++++++- modules/packages/npm/metadata.go | 2 +- routers/web/repo/editor_cherry_pick.go | 4 +--- services/mailer/sender/sender.go | 31 ++++++++++---------------- services/repository/generate.go | 4 ++-- services/repository/generate_test.go | 21 ++++++++++------- templates/package/content/pypi.tmpl | 2 +- 8 files changed, 80 insertions(+), 37 deletions(-) diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 11b5123c27..cc7695726b 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -62,7 +62,28 @@ type PackageMetadata struct { Author User `json:"author"` ReadmeFilename string `json:"readmeFilename,omitempty"` Users map[string]bool `json:"users,omitempty"` - License string `json:"license,omitempty"` + License License `json:"license,omitempty"` +} + +type License string + +func (l *License) UnmarshalJSON(data []byte) error { + switch data[0] { + case '"': + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *l = License(value) + case '{': + var values map[string]any + if err := json.Unmarshal(data, &values); err != nil { + return err + } + value, _ := values["type"].(string) + *l = License(value) + } + return nil } // PackageMetadataVersion documentation: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version @@ -74,7 +95,7 @@ type PackageMetadataVersion struct { Description string `json:"description"` Author User `json:"author"` Homepage string `json:"homepage,omitempty"` - License string `json:"license,omitempty"` + License License `json:"license,omitempty"` Repository Repository `json:"repository"` Keywords []string `json:"keywords,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"` diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index 806377a52b..40c50de91f 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/json" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParsePackage(t *testing.T) { @@ -291,11 +292,36 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, packageDescription, p.Metadata.Readme) assert.Equal(t, packageAuthor, p.Metadata.Author) assert.Equal(t, packageBin, p.Metadata.Bin["bin"]) - assert.Equal(t, "MIT", p.Metadata.License) + assert.Equal(t, "MIT", string(p.Metadata.License)) assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL) assert.Contains(t, p.Metadata.Dependencies, "package") assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"]) assert.Equal(t, repository.Type, p.Metadata.Repository.Type) assert.Equal(t, repository.URL, p.Metadata.Repository.URL) }) + + t.Run("ValidLicenseMap", func(t *testing.T) { + packageJSON := `{ + "versions": { + "0.1.1": { + "name": "dev-null", + "version": "0.1.1", + "license": { + "type": "MIT" + }, + "dist": { + "integrity": "sha256-" + } + } + }, + "_attachments": { + "foo": { + "data": "AAAA" + } + } +}` + p, err := ParsePackage(strings.NewReader(packageJSON)) + require.NoError(t, err) + require.Equal(t, "MIT", string(p.Metadata.License)) + }) } diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index 362d0470d5..e6bbcb1177 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -12,7 +12,7 @@ type Metadata struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Author string `json:"author,omitempty"` - License string `json:"license,omitempty"` + License License `json:"license,omitempty"` ProjectURL string `json:"project_url,omitempty"` Keywords []string `json:"keywords,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"` diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go index c1f3ae861b..ca0e19517a 100644 --- a/routers/web/repo/editor_cherry_pick.go +++ b/routers/web/repo/editor_cherry_pick.go @@ -36,9 +36,7 @@ func CherryPick(ctx *context.Context) { ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() } else { ctx.Data["CherryPickType"] = "cherry-pick" - splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) - ctx.Data["commit_summary"] = splits[0] - ctx.Data["commit_message"] = splits[1] + ctx.Data["commit_summary"], ctx.Data["commit_message"], _ = strings.Cut(cherryPickCommit.Message(), "\n") } ctx.HTML(http.StatusOK, tplCherryPick) diff --git a/services/mailer/sender/sender.go b/services/mailer/sender/sender.go index e470c2f2b3..30c6feaf7a 100644 --- a/services/mailer/sender/sender.go +++ b/services/mailer/sender/sender.go @@ -4,10 +4,8 @@ package sender import ( + "errors" "io" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" ) type Sender interface { @@ -16,23 +14,18 @@ type Sender interface { var Send = send -func send(sender Sender, msgs ...*Message) error { - if setting.MailService == nil { - log.Error("Mailer: Send is being invoked but mail service hasn't been initialized") - return nil +func send(sender Sender, msg *Message) error { + m := msg.ToMessage() + froms := m.GetFrom() + to, err := m.GetRecipients() + if err != nil { + return err } - for _, msg := range msgs { - m := msg.ToMessage() - froms := m.GetFrom() - to, err := m.GetRecipients() - if err != nil { - return err - } - // TODO: implement sending from multiple addresses - if err := sender.Send(froms[0].Address, to, m); err != nil { - return err - } + // TODO: implement sending from multiple addresses + if len(froms) == 0 { + // FIXME: no idea why sometimes the "froms" can be empty, need to figure out the root problem + return errors.New("no FROM specified") } - return nil + return sender.Send(froms[0].Address, to, m) } diff --git a/services/repository/generate.go b/services/repository/generate.go index 3ec31dac22..b2913cd110 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -177,7 +177,7 @@ func substGiteaTemplateFile(ctx context.Context, tmpDir, tmpDirSubPath string, t } generatedContent := generateExpansion(ctx, string(content), templateRepo, generateRepo) - substSubPath := filepath.Clean(filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo))) + substSubPath := filePathSanitize(generateExpansion(ctx, tmpDirSubPath, templateRepo, generateRepo)) newLocalPath := filepath.Join(tmpDir, substSubPath) regular, err := util.IsRegularFile(newLocalPath) if canWrite := regular || errors.Is(err, fs.ErrNotExist); !canWrite { @@ -358,5 +358,5 @@ func filePathSanitize(s string) string { } fields[i] = field } - return filepath.FromSlash(strings.Join(fields, "/")) + return filepath.Clean(filepath.FromSlash(strings.Trim(strings.Join(fields, "/"), "/"))) } diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go index 9c01911ded..432de4dc59 100644 --- a/services/repository/generate_test.go +++ b/services/repository/generate_test.go @@ -54,19 +54,24 @@ text/*.txt } func TestFilePathSanitize(t *testing.T) { - assert.Equal(t, "test_CON", filePathSanitize("test_CON")) - assert.Equal(t, "test CON", filePathSanitize("test CON ")) - assert.Equal(t, "__/traverse/__", filePathSanitize(".. /traverse/ ..")) - assert.Equal(t, "./__/a/_git/b_", filePathSanitize("./../a/.git/ b: ")) + // path clean + assert.Equal(t, "a", filePathSanitize("//a/")) + assert.Equal(t, "_a", filePathSanitize(`\a`)) + assert.Equal(t, "__/a/__", filePathSanitize(".. /a/ ..")) + assert.Equal(t, "__/a/_git/b_", filePathSanitize("./../a/.git/ b: ")) + + // Windows reserved names assert.Equal(t, "_", filePathSanitize("CoN")) assert.Equal(t, "_", filePathSanitize("LpT1")) assert.Equal(t, "_", filePathSanitize("CoM1")) + assert.Equal(t, "test_CON", filePathSanitize("test_CON")) + assert.Equal(t, "test CON", filePathSanitize("test CON ")) + + // special chars assert.Equal(t, "_", filePathSanitize("\u0000")) - assert.Equal(t, "目标", filePathSanitize("目标")) - // unlike filepath.Clean, it only sanitizes, doesn't change the separator layout - assert.Equal(t, "", filePathSanitize("")) //nolint:testifylint // for easy reading + assert.Equal(t, ".", filePathSanitize("")) assert.Equal(t, ".", filePathSanitize(".")) - assert.Equal(t, "/", filePathSanitize("/")) + assert.Equal(t, ".", filePathSanitize("/")) } func TestProcessGiteaTemplateFile(t *testing.T) { diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl index 2625c160fe..15d8971eaa 100644 --- a/templates/package/content/pypi.tmpl +++ b/templates/package/content/pypi.tmpl @@ -4,7 +4,7 @@
-
pip install --index-url  --extra-index-url https://pypi.org/ {{.PackageDescriptor.Package.Name}}
+
pip install --index-url  --extra-index-url https://pypi.org/simple {{.PackageDescriptor.Package.Name}}