mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-10 00:26:14 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
144 files changed
+3052
-2074
No files matched your search
@@ -162,7 +162,7 @@ func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer *
|
||||
}
|
||||
}
|
||||
|
||||
defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
|
||||
defaultGPGSettings, err := git.GetDefaultPublicGPGKey(ctx, false)
|
||||
if err != nil {
|
||||
log.Error("Error getting default public gpg key: %v", err)
|
||||
} else if defaultGPGSettings == nil {
|
||||
|
||||
+14
-48
@@ -108,34 +108,34 @@ func IsErrWontSign(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// PublicSigningKey gets the public signing key within a provided repository directory
|
||||
func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) {
|
||||
signingKey, _ := git.GetSigningKey(ctx, repoPath)
|
||||
// PublicSigningKey gets the public signing key of the entire instance
|
||||
func PublicSigningKey(ctx context.Context) (content, format string, err error) {
|
||||
signingKey, _ := git.GetSigningKey(ctx)
|
||||
if signingKey == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
if signingKey.Format == git.SigningKeyFormatSSH {
|
||||
content, err := os.ReadFile(signingKey.KeyID)
|
||||
if err != nil {
|
||||
log.Error("Unable to read SSH public key file in %s: %s, %v", repoPath, signingKey, err)
|
||||
log.Error("Unable to read SSH public key file: %s, %v", signingKey, err)
|
||||
return "", signingKey.Format, err
|
||||
}
|
||||
return string(content), signingKey.Format, nil
|
||||
}
|
||||
|
||||
content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
|
||||
content, stderr, err := process.GetManager().ExecDir(ctx, -1, setting.Git.HomePath,
|
||||
"gpg --export -a", "gpg", "--export", "-a", signingKey.KeyID)
|
||||
if err != nil {
|
||||
log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
|
||||
log.Error("Unable to get default signing key: %s, %s, %v", signingKey, stderr, err)
|
||||
return "", signingKey.Format, err
|
||||
}
|
||||
return content, signingKey.Format, nil
|
||||
}
|
||||
|
||||
// SignInitialCommit determines if we should sign the initial commit to this repository
|
||||
func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
func SignInitialCommit(ctx context.Context, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
|
||||
signingKey, sig := git.GetSigningKey(ctx, repoPath)
|
||||
signingKey, sig := git.GetSigningKey(ctx)
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
@@ -169,9 +169,9 @@ Loop:
|
||||
}
|
||||
|
||||
// SignWikiCommit determines if we should sign the commits to this repository wiki
|
||||
func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
|
||||
signingKey, sig := gitrepo.GetSigningKey(ctx, repo.WikiStorageRepo())
|
||||
signingKey, sig := gitrepo.GetSigningKey(ctx)
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
@@ -200,11 +200,6 @@ Loop:
|
||||
return false, nil, nil, &ErrWontSign{twofa}
|
||||
}
|
||||
case parentSigned:
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commit, err := gitRepo.GetCommit("HEAD")
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
@@ -222,9 +217,9 @@ Loop:
|
||||
}
|
||||
|
||||
// SignCRUDAction determines if we should sign a CRUD commit to this repository
|
||||
func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
func SignCRUDAction(ctx context.Context, u *user_model.User, gitRepo *git.Repository, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
|
||||
signingKey, sig := git.GetSigningKey(ctx, repoPath)
|
||||
signingKey, sig := git.GetSigningKey(ctx)
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
@@ -253,11 +248,6 @@ Loop:
|
||||
return false, nil, nil, &ErrWontSign{twofa}
|
||||
}
|
||||
case parentSigned:
|
||||
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
isEmpty, err := gitRepo.IsEmpty()
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
@@ -281,22 +271,19 @@ Loop:
|
||||
}
|
||||
|
||||
// SignMerge determines if we should sign a PR merge commit to the base repository
|
||||
func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, gitRepo *git.Repository, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) {
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("Unable to get Base Repo for pull request")
|
||||
return false, nil, nil, err
|
||||
}
|
||||
repo := pr.BaseRepo
|
||||
|
||||
signingKey, signer := gitrepo.GetSigningKey(ctx, repo)
|
||||
signingKey, signer := gitrepo.GetSigningKey(ctx)
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
|
||||
|
||||
var gitRepo *git.Repository
|
||||
var err error
|
||||
|
||||
Loop:
|
||||
for _, rule := range rules {
|
||||
switch rule {
|
||||
@@ -332,13 +319,6 @@ Loop:
|
||||
return false, nil, nil, &ErrWontSign{approved}
|
||||
}
|
||||
case baseSigned:
|
||||
if gitRepo == nil {
|
||||
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
}
|
||||
commit, err := gitRepo.GetCommit(baseCommit)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
@@ -348,13 +328,6 @@ Loop:
|
||||
return false, nil, nil, &ErrWontSign{baseSigned}
|
||||
}
|
||||
case headSigned:
|
||||
if gitRepo == nil {
|
||||
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
}
|
||||
commit, err := gitRepo.GetCommit(headCommit)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
@@ -364,13 +337,6 @@ Loop:
|
||||
return false, nil, nil, &ErrWontSign{headSigned}
|
||||
}
|
||||
case commitsSigned:
|
||||
if gitRepo == nil {
|
||||
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
}
|
||||
commit, err := gitRepo.GetCommit(headCommit)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
|
||||
@@ -20,15 +20,27 @@ import (
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// RedirectToUser redirect to a differently-named user
|
||||
func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
|
||||
func RedirectToUser(ctx *Base, doer *user_model.User, userName string, redirectUserID int64) {
|
||||
user, err := user_model.GetUserByID(ctx, redirectUserID)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "unable to get user")
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.HTTPError(http.StatusNotFound, "user does not exist")
|
||||
} else {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "unable to get user")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Visibility
|
||||
if user.Visibility != structs.VisibleTypePublic && doer == nil {
|
||||
// We must be signed in to see limited or private organizations
|
||||
ctx.HTTPError(http.StatusNotFound, "user does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func GetOrganizationByParams(ctx *Context) {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
redirectUserID, err := user_model.LookupUserRedirect(ctx, orgName)
|
||||
if err == nil {
|
||||
RedirectToUser(ctx.Base, orgName, redirectUserID)
|
||||
RedirectToUser(ctx.Base, ctx.Doer, orgName, redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
@@ -70,8 +70,9 @@ type OrgAssignmentOptions struct {
|
||||
}
|
||||
|
||||
// OrgAssignment returns a middleware to handle organization assignment
|
||||
func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) {
|
||||
func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
|
||||
return func(ctx *Context) {
|
||||
opts := orgAssignmentOpts // it must be a copy, because the values will be changed
|
||||
var err error
|
||||
if ctx.ContextUser == nil {
|
||||
// if Organization is not defined, get it from params
|
||||
|
||||
@@ -140,7 +140,13 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
|
||||
protectionRequireSigned = protectedBranch.RequireSignedCommits
|
||||
}
|
||||
|
||||
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
|
||||
targetGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, targetRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, doer, targetGitRepo, refName.String())
|
||||
wontSignReason := ""
|
||||
if asymkey_service.IsErrWontSign(err) {
|
||||
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
|
||||
@@ -443,7 +449,7 @@ func RepoAssignment(ctx *Context) {
|
||||
}
|
||||
|
||||
if redirectUserID, err := user_model.LookupUserRedirect(ctx, userName); err == nil {
|
||||
RedirectToUser(ctx.Base, userName, redirectUserID)
|
||||
RedirectToUser(ctx.Base, ctx.Doer, userName, redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
|
||||
@@ -69,7 +69,7 @@ func userAssignment(ctx *Base, doer *user_model.User, errCb func(int, any)) (con
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
if redirectUserID, err := user_model.LookupUserRedirect(ctx, username); err == nil {
|
||||
RedirectToUser(ctx, username, redirectUserID)
|
||||
RedirectToUser(ctx, doer, username, redirectUserID)
|
||||
} else if user_model.IsErrUserRedirectNotExist(err) {
|
||||
errCb(http.StatusNotFound, err)
|
||||
} else {
|
||||
|
||||
@@ -5,12 +5,10 @@ package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func checkOldArchives(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||
@@ -21,18 +19,18 @@ func checkOldArchives(ctx context.Context, logger log.Logger, autofix bool) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
p := filepath.Join(repo.RepoPath(), "archives")
|
||||
isDir, err := util.IsDir(p)
|
||||
isDir, err := gitrepo.IsRepoDirExist(ctx, repo, "archives")
|
||||
if err != nil {
|
||||
log.Warn("check if %s is directory failed: %v", p, err)
|
||||
log.Warn("check if %s is directory failed: %v", repo.FullName(), err)
|
||||
}
|
||||
if isDir {
|
||||
numRepos++
|
||||
if autofix {
|
||||
if err := os.RemoveAll(p); err == nil {
|
||||
err := gitrepo.RemoveRepoFileOrDir(ctx, repo, "archives")
|
||||
if err == nil {
|
||||
numReposUpdated++
|
||||
} else {
|
||||
log.Warn("remove %s failed: %v", p, err)
|
||||
log.Warn("remove %s failed: %v", repo.FullName(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) err
|
||||
numNeedUpdate++
|
||||
if autofix {
|
||||
if !isPublic && isExist {
|
||||
if err = gitrepo.RemoveRepoFile(ctx, repo, daemonExportFile); err != nil {
|
||||
if err = gitrepo.RemoveRepoFileOrDir(ctx, repo, daemonExportFile); err != nil {
|
||||
log.Error("Failed to remove %s:%s: %v", repo.FullName(), daemonExportFile, err)
|
||||
}
|
||||
} else if isPublic && !isExist {
|
||||
|
||||
@@ -648,6 +648,19 @@ func (f *NewReleaseForm) Validate(req *http.Request, errs binding.Errors) bindin
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// GenerateReleaseNotesForm retrieves release notes recommendations.
|
||||
type GenerateReleaseNotesForm struct {
|
||||
TagName string `form:"tag_name" binding:"Required;GitRefName;MaxSize(255)"`
|
||||
TagTarget string `form:"tag_target" binding:"MaxSize(255)"`
|
||||
PreviousTag string `form:"previous_tag" binding:"MaxSize(255)"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *GenerateReleaseNotesForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// EditReleaseForm form for changing release
|
||||
type EditReleaseForm struct {
|
||||
Title string `form:"title" binding:"Required;MaxSize(255)"`
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type DiffTree struct {
|
||||
@@ -56,7 +57,9 @@ func runGitDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase b
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := gitcmd.NewCommand("diff-tree", "--raw", "-r", "--find-renames", "--root")
|
||||
cmd := gitcmd.NewCommand("diff-tree", "--raw", "-r", "--root").
|
||||
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold)
|
||||
|
||||
if useMergeBase {
|
||||
cmd.AddArguments("--merge-base")
|
||||
}
|
||||
|
||||
+16
-14
@@ -835,11 +835,11 @@ parsingLoop:
|
||||
if buffer.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
charsetLabel, err := charset.DetectEncoding(buffer.Bytes())
|
||||
if charsetLabel != "UTF-8" && err == nil {
|
||||
encoding, _ := stdcharset.Lookup(charsetLabel)
|
||||
if encoding != nil {
|
||||
diffLineTypeDecoders[lineType] = encoding.NewDecoder()
|
||||
charsetLabel, _ := charset.DetectEncoding(buffer.Bytes())
|
||||
if charsetLabel != "UTF-8" {
|
||||
charsetEncoding, _ := stdcharset.Lookup(charsetLabel)
|
||||
if charsetEncoding != nil {
|
||||
diffLineTypeDecoders[lineType] = charsetEncoding.NewDecoder()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1225,8 +1225,9 @@ func getDiffBasic(ctx context.Context, gitRepo *git.Repository, opts *DiffOption
|
||||
}
|
||||
|
||||
cmdDiff := gitcmd.NewCommand().
|
||||
AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
|
||||
AddArguments(opts.WhitespaceBehavior...)
|
||||
AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/").
|
||||
AddArguments(opts.WhitespaceBehavior...).
|
||||
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold)
|
||||
|
||||
// In git 2.31, git diff learned --skip-to which we can use to shortcut skip to file
|
||||
// so if we are using at least this version of git we don't have to tell ParsePatch to do
|
||||
@@ -1325,10 +1326,10 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
|
||||
if shouldFullFileHighlight {
|
||||
if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
|
||||
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.String())
|
||||
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes())
|
||||
}
|
||||
if limitedContent.RightContent != nil && limitedContent.RightContent.buf.Len() < MaxDiffHighlightEntireFileSize {
|
||||
diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.String())
|
||||
diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1336,10 +1337,11 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func highlightCodeLines(diffFile *DiffFile, isLeft bool, content string) map[int]template.HTML {
|
||||
func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
|
||||
content := util.UnsafeBytesToString(charset.ToUTF8(rawContent, charset.ConvertOpts{}))
|
||||
highlightedNewContent, _ := highlight.Code(diffFile.Name, diffFile.Language, content)
|
||||
splitLines := strings.Split(string(highlightedNewContent), "\n")
|
||||
lines := make(map[int]template.HTML, len(splitLines))
|
||||
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlightedNewContent)
|
||||
lines := make(map[int]template.HTML, len(unsafeLines))
|
||||
// only save the highlighted lines we need, but not the whole file, to save memory
|
||||
for _, sec := range diffFile.Sections {
|
||||
for _, ln := range sec.Lines {
|
||||
@@ -1349,8 +1351,8 @@ func highlightCodeLines(diffFile *DiffFile, isLeft bool, content string) map[int
|
||||
}
|
||||
if lineIdx >= 1 {
|
||||
idx := lineIdx - 1
|
||||
if idx < len(splitLines) {
|
||||
lines[idx] = template.HTML(splitLines[idx])
|
||||
if idx < len(unsafeLines) {
|
||||
lines[idx] = template.HTML(util.UnsafeBytesToString(unsafeLines[idx]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package gitdiff
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -1106,3 +1107,41 @@ func TestDiffLine_GetExpandDirection(t *testing.T) {
|
||||
assert.Equal(t, c.direction, c.diffLine.GetExpandDirection(), "case %s expected direction: %s", c.name, c.direction)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighlightCodeLines(t *testing.T) {
|
||||
t.Run("CharsetDetecting", func(t *testing.T) {
|
||||
diffFile := &DiffFile{
|
||||
Name: "a.c",
|
||||
Language: "c",
|
||||
Sections: []*DiffSection{
|
||||
{
|
||||
Lines: []*DiffLine{{LeftIdx: 1}},
|
||||
},
|
||||
},
|
||||
}
|
||||
ret := highlightCodeLines(diffFile, true, []byte("// abc\xcc def\xcd")) // ISO-8859-1 bytes
|
||||
assert.Equal(t, "<span class=\"c1\">// abcÌ defÍ\n</span>", string(ret[0]))
|
||||
})
|
||||
|
||||
t.Run("LeftLines", func(t *testing.T) {
|
||||
diffFile := &DiffFile{
|
||||
Name: "a.c",
|
||||
Language: "c",
|
||||
Sections: []*DiffSection{
|
||||
{
|
||||
Lines: []*DiffLine{
|
||||
{LeftIdx: 1},
|
||||
{LeftIdx: 2},
|
||||
{LeftIdx: 3},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
const nl = "\n"
|
||||
ret := highlightCodeLines(diffFile, true, []byte("a\nb\n"))
|
||||
assert.Equal(t, map[int]template.HTML{
|
||||
0: `<span class="n">a</span>` + nl,
|
||||
1: `<span class="n">b</span>`,
|
||||
}, ret)
|
||||
})
|
||||
}
|
||||
@@ -25,12 +25,12 @@ func TestDiffWithHighlight(t *testing.T) {
|
||||
|
||||
t.Run("CleanUp", func(t *testing.T) {
|
||||
hcd := newHighlightCodeDiff()
|
||||
codeA := template.HTML(`<span class="cm>this is a comment</span>`)
|
||||
codeB := template.HTML(`<span class="cm>this is updated comment</span>`)
|
||||
codeA := template.HTML(`<span class="cm">this is a comment</span>`)
|
||||
codeB := template.HTML(`<span class="cm">this is updated comment</span>`)
|
||||
outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||
assert.Equal(t, `<span class="cm>this is <span class="removed-code">a</span> comment</span>`, string(outDel))
|
||||
assert.Equal(t, `<span class="cm">this is <span class="removed-code">a</span> comment</span>`, string(outDel))
|
||||
outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB)
|
||||
assert.Equal(t, `<span class="cm>this is <span class="added-code">updated</span> comment</span>`, string(outAdd))
|
||||
assert.Equal(t, `<span class="cm">this is <span class="added-code">updated</span> comment</span>`, string(outAdd))
|
||||
})
|
||||
|
||||
t.Run("OpenCloseTags", func(t *testing.T) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func TestRenderHelperCodePreview(t *testing.T) {
|
||||
<td class="lines-code chroma"><div class="code-inner"><span class="gh"># repo1</div></td>
|
||||
</tr><tr>
|
||||
<td class="lines-num"><span data-line-number="2"></span></td>
|
||||
<td class="lines-code chroma"><div class="code-inner"></span><span class="gh"></span></div></td>
|
||||
<td class="lines-code chroma"><div class="code-inner"></span></div></td>
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -306,14 +306,15 @@ func (g *RepositoryDumper) CreateReleases(_ context.Context, releases ...*base.R
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
} else {
|
||||
resp, err := http.Get(*asset.DownloadURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
rc = resp.Body
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
fw, err := os.Create(attachPath)
|
||||
if err != nil {
|
||||
@@ -357,6 +358,11 @@ func (g *RepositoryDumper) SyncTags(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncBranches syncs branches in the database
|
||||
func (g *RepositoryDumper) SyncBranches(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateIssues creates issues
|
||||
func (g *RepositoryDumper) CreateIssues(_ context.Context, issues ...*base.Issue) error {
|
||||
var err error
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
t.Skipf("Can't reach https://gitea.com, skipping %s", t.Name())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ctx := t.Context()
|
||||
downloader, err := NewGiteaDownloader(ctx, "https://gitea.com", "gitea/test_repo", "", "", giteaToken)
|
||||
require.NoError(t, err, "NewGiteaDownloader error occur")
|
||||
|
||||
@@ -368,6 +368,11 @@ func (g *GiteaLocalUploader) SyncTags(ctx context.Context) error {
|
||||
return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo)
|
||||
}
|
||||
|
||||
func (g *GiteaLocalUploader) SyncBranches(ctx context.Context) error {
|
||||
_, err := repo_module.SyncRepoBranchesWithRepo(ctx, g.repo, g.gitRepo, g.doer.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateIssues creates issues
|
||||
func (g *GiteaLocalUploader) CreateIssues(ctx context.Context, issues ...*base.Issue) error {
|
||||
iss := make([]*issues_model.Issue, 0, len(issues))
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -17,15 +14,10 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
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/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
base "code.gitea.io/gitea/modules/migration"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -228,297 +220,3 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, linkedUser.ID, target.GetUserID())
|
||||
}
|
||||
|
||||
func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
//
|
||||
// fromRepo master
|
||||
//
|
||||
fromRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
baseRef := "master"
|
||||
// this is very different from the real situation. It should be a bare repository for all the Gitea managed repositories
|
||||
assert.NoError(t, git.InitRepository(t.Context(), fromRepo.RepoPath(), false, fromRepo.ObjectFormatName))
|
||||
err := gitrepo.RunCmd(t.Context(), fromRepo, gitcmd.NewCommand("symbolic-ref").AddDynamicArguments("HEAD", git.BranchPrefix+baseRef))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("# Testing Repository\n\nOriginally created in: "+fromRepo.RepoPath()), 0o644))
|
||||
assert.NoError(t, git.AddChanges(t.Context(), fromRepo.RepoPath(), true))
|
||||
signature := git.Signature{
|
||||
Email: "test@example.com",
|
||||
Name: "test",
|
||||
When: time.Now(),
|
||||
}
|
||||
assert.NoError(t, git.CommitChanges(t.Context(), fromRepo.RepoPath(), git.CommitChangesOptions{
|
||||
Committer: &signature,
|
||||
Author: &signature,
|
||||
Message: "Initial Commit",
|
||||
}))
|
||||
fromGitRepo, err := gitrepo.OpenRepository(t.Context(), fromRepo)
|
||||
assert.NoError(t, err)
|
||||
defer fromGitRepo.Close()
|
||||
baseSHA, err := fromGitRepo.GetBranchCommitID(baseRef)
|
||||
assert.NoError(t, err)
|
||||
|
||||
//
|
||||
// fromRepo branch1
|
||||
//
|
||||
headRef := "branch1"
|
||||
_, err = gitrepo.RunCmdString(t.Context(), fromRepo, gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(headRef))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, os.WriteFile(filepath.Join(fromRepo.RepoPath(), "README.md"), []byte("SOMETHING"), 0o644))
|
||||
assert.NoError(t, git.AddChanges(t.Context(), fromRepo.RepoPath(), true))
|
||||
signature.When = time.Now()
|
||||
assert.NoError(t, git.CommitChanges(t.Context(), fromRepo.RepoPath(), git.CommitChangesOptions{
|
||||
Committer: &signature,
|
||||
Author: &signature,
|
||||
Message: "Pull request",
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
headSHA, err := fromGitRepo.GetBranchCommitID(headRef)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fromRepoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: fromRepo.OwnerID})
|
||||
|
||||
//
|
||||
// forkRepo branch2
|
||||
//
|
||||
forkHeadRef := "branch2"
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 8})
|
||||
assert.NoError(t, git.Clone(t.Context(), fromRepo.RepoPath(), forkRepo.RepoPath(), git.CloneRepoOptions{
|
||||
Branch: headRef,
|
||||
}))
|
||||
_, err = gitrepo.RunCmdString(t.Context(), forkRepo, gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(forkHeadRef))
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, os.WriteFile(filepath.Join(forkRepo.RepoPath(), "README.md"), []byte("# branch2 "+forkRepo.RepoPath()), 0o644))
|
||||
assert.NoError(t, git.AddChanges(t.Context(), forkRepo.RepoPath(), true))
|
||||
assert.NoError(t, git.CommitChanges(t.Context(), forkRepo.RepoPath(), git.CommitChangesOptions{
|
||||
Committer: &signature,
|
||||
Author: &signature,
|
||||
Message: "branch2 commit",
|
||||
}))
|
||||
forkGitRepo, err := gitrepo.OpenRepository(t.Context(), forkRepo)
|
||||
assert.NoError(t, err)
|
||||
defer forkGitRepo.Close()
|
||||
forkHeadSHA, err := forkGitRepo.GetBranchCommitID(forkHeadRef)
|
||||
assert.NoError(t, err)
|
||||
|
||||
toRepoName := "migrated"
|
||||
ctx := t.Context()
|
||||
uploader := NewGiteaLocalUploader(ctx, fromRepoOwner, fromRepoOwner.Name, toRepoName)
|
||||
uploader.gitServiceType = structs.GiteaService
|
||||
|
||||
assert.NoError(t, repo_service.Init(t.Context()))
|
||||
assert.NoError(t, uploader.CreateRepo(ctx, &base.Repository{
|
||||
Description: "description",
|
||||
OriginalURL: fromRepo.RepoPath(),
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
IsPrivate: false,
|
||||
IsMirror: true,
|
||||
}, base.MigrateOptions{
|
||||
GitServiceType: structs.GiteaService,
|
||||
Private: false,
|
||||
Mirror: true,
|
||||
}))
|
||||
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
head string
|
||||
logFilter []string
|
||||
logFiltered []bool
|
||||
pr base.PullRequest
|
||||
}{
|
||||
{
|
||||
name: "fork, good Head.SHA",
|
||||
head: fmt.Sprintf("%s/%s", forkRepo.OwnerName, forkHeadRef),
|
||||
pr: base.PullRequest{
|
||||
PatchURL: "",
|
||||
Number: 1,
|
||||
State: "open",
|
||||
Base: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: baseRef,
|
||||
SHA: baseSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
Head: base.PullRequestBranch{
|
||||
CloneURL: forkRepo.RepoPath(),
|
||||
Ref: forkHeadRef,
|
||||
SHA: forkHeadSHA,
|
||||
RepoName: forkRepo.Name,
|
||||
OwnerName: forkRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fork, invalid Head.Ref",
|
||||
head: "unknown repository",
|
||||
pr: base.PullRequest{
|
||||
PatchURL: "",
|
||||
Number: 1,
|
||||
State: "open",
|
||||
Base: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: baseRef,
|
||||
SHA: baseSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
Head: base.PullRequestBranch{
|
||||
CloneURL: forkRepo.RepoPath(),
|
||||
Ref: "INVALID",
|
||||
SHA: forkHeadSHA,
|
||||
RepoName: forkRepo.Name,
|
||||
OwnerName: forkRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
logFilter: []string{"Fetch branch from"},
|
||||
logFiltered: []bool{true},
|
||||
},
|
||||
{
|
||||
name: "invalid fork CloneURL",
|
||||
head: "unknown repository",
|
||||
pr: base.PullRequest{
|
||||
PatchURL: "",
|
||||
Number: 1,
|
||||
State: "open",
|
||||
Base: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: baseRef,
|
||||
SHA: baseSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
Head: base.PullRequestBranch{
|
||||
CloneURL: "UNLIKELY",
|
||||
Ref: forkHeadRef,
|
||||
SHA: forkHeadSHA,
|
||||
RepoName: forkRepo.Name,
|
||||
OwnerName: "WRONG",
|
||||
},
|
||||
},
|
||||
logFilter: []string{"AddRemote"},
|
||||
logFiltered: []bool{true},
|
||||
},
|
||||
{
|
||||
name: "no fork, good Head.SHA",
|
||||
head: headRef,
|
||||
pr: base.PullRequest{
|
||||
PatchURL: "",
|
||||
Number: 1,
|
||||
State: "open",
|
||||
Base: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: baseRef,
|
||||
SHA: baseSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
Head: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: headRef,
|
||||
SHA: headSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no fork, empty Head.SHA",
|
||||
head: headRef,
|
||||
pr: base.PullRequest{
|
||||
PatchURL: "",
|
||||
Number: 1,
|
||||
State: "open",
|
||||
Base: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: baseRef,
|
||||
SHA: baseSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
Head: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: headRef,
|
||||
SHA: "",
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
logFilter: []string{"Empty reference", "Cannot remove local head"},
|
||||
logFiltered: []bool{true, false},
|
||||
},
|
||||
{
|
||||
name: "no fork, invalid Head.SHA",
|
||||
head: headRef,
|
||||
pr: base.PullRequest{
|
||||
PatchURL: "",
|
||||
Number: 1,
|
||||
State: "open",
|
||||
Base: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: baseRef,
|
||||
SHA: baseSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
Head: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: headRef,
|
||||
SHA: "brokenSHA",
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
logFilter: []string{"Deprecated local head"},
|
||||
logFiltered: []bool{true},
|
||||
},
|
||||
{
|
||||
name: "no fork, not found Head.SHA",
|
||||
head: headRef,
|
||||
pr: base.PullRequest{
|
||||
PatchURL: "",
|
||||
Number: 1,
|
||||
State: "open",
|
||||
Base: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: baseRef,
|
||||
SHA: baseSHA,
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
Head: base.PullRequestBranch{
|
||||
CloneURL: fromRepo.RepoPath(),
|
||||
Ref: headRef,
|
||||
SHA: "2697b352310fcd01cbd1f3dbd43b894080027f68",
|
||||
RepoName: fromRepo.Name,
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
logFilter: []string{"Deprecated local head", "Cannot remove local head"},
|
||||
logFiltered: []bool{true, false},
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
stopMark := fmt.Sprintf(">>>>>>>>>>>>>STOP: %s<<<<<<<<<<<<<<<", testCase.name)
|
||||
|
||||
logChecker, cleanup := test.NewLogChecker(log.DEFAULT)
|
||||
logChecker.Filter(testCase.logFilter...).StopMark(stopMark)
|
||||
defer cleanup()
|
||||
|
||||
testCase.pr.EnsuredSafe = true
|
||||
|
||||
head, err := uploader.updateGitForPullRequest(ctx, &testCase.pr)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testCase.head, head)
|
||||
|
||||
log.Info(stopMark)
|
||||
|
||||
logFiltered, logStopped := logChecker.Check(5 * time.Second)
|
||||
assert.True(t, logStopped)
|
||||
if len(testCase.logFilter) > 0 {
|
||||
assert.Equal(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
t.Skipf("Can't access test repo, skipping %s", t.Name())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ctx := t.Context()
|
||||
downloader, err := NewGitlabDownloader(ctx, "https://gitlab.com", "gitea/test_repo", gitlabPersonalAccessToken)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestGogsDownloadRepo(t *testing.T) {
|
||||
t.Skipf("visit test repo failed, ignored")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ctx := t.Context()
|
||||
downloader := NewGogsDownloader(ctx, "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO")
|
||||
repo, err := downloader.GetRepoInfo(ctx)
|
||||
|
||||
@@ -478,6 +478,15 @@ func migrateRepository(ctx context.Context, doer *user_model.User, downloader ba
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(mapInsertedPRIndexes) > 0 {
|
||||
// The pull requests migrating process may created head branches in the base repository
|
||||
// because head repository maybe a fork one which will not be migrated. So that we need
|
||||
// to sync branches again.
|
||||
log.Trace("syncing branches after migrating pull requests")
|
||||
if err = uploader.SyncBranches(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Comments && supportAllComments {
|
||||
|
||||
@@ -77,19 +77,19 @@ type OneDevDownloader struct {
|
||||
}
|
||||
|
||||
// NewOneDevDownloader creates a new downloader
|
||||
func NewOneDevDownloader(_ context.Context, baseURL *url.URL, username, password, repoPath string) *OneDevDownloader {
|
||||
func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, password, repoPath string) *OneDevDownloader {
|
||||
httpTransport := NewMigrationHTTPTransport()
|
||||
downloader := &OneDevDownloader{
|
||||
baseURL: baseURL,
|
||||
repoPath: repoPath,
|
||||
client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||
if len(username) > 0 && len(password) > 0 {
|
||||
Transport: roundTripperFunc(
|
||||
func(req *http.Request) (*http.Response, error) {
|
||||
if username != "" && password != "" {
|
||||
req.SetBasicAuth(username, password)
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
return httpTransport.RoundTrip(req.WithContext(ctx))
|
||||
}),
|
||||
},
|
||||
userMap: make(map[int64]*onedevUser),
|
||||
milestoneMap: make(map[int64]string),
|
||||
|
||||
@@ -19,6 +19,7 @@ func TestOneDevDownloadRepo(t *testing.T) {
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
t.Skipf("Can't access test repo, skipping %s", t.Name())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
u, _ := url.Parse("https://code.onedev.io")
|
||||
ctx := t.Context()
|
||||
|
||||
@@ -232,7 +232,13 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
|
||||
return true, nil
|
||||
}
|
||||
|
||||
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName())
|
||||
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
sign, _, _, err := asymkey_service.SignMerge(ctx, pr, doer, gitRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
||||
|
||||
return sign, err
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
|
||||
)
|
||||
|
||||
// We don't need a temporary remote for same repository.
|
||||
if headGitRepo.Path != baseRepo.RepoPath() {
|
||||
if baseRepo.ID != headRepo.ID {
|
||||
// Add a temporary remote
|
||||
tmpRemote = strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
if err = gitrepo.GitRemoteAdd(ctx, headRepo, tmpRemote, baseRepo.RepoPath()); err != nil {
|
||||
|
||||
@@ -84,7 +84,7 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
|
||||
if err != nil {
|
||||
defer cancel()
|
||||
log.Error("failed to get sha of head branch in %-v: show-ref[%s] --hash refs/heads/tracking: %v", mergeCtx.pr, mergeCtx.tmpBasePath, err)
|
||||
return nil, nil, fmt.Errorf("unable to get sha of head branch in %v %w", pr, err)
|
||||
return nil, nil, fmt.Errorf("unable to get sha of head branch in pr[%d]: %w", pr.ID, err)
|
||||
}
|
||||
if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID {
|
||||
defer cancel()
|
||||
@@ -105,8 +105,15 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
|
||||
mergeCtx.sig = doer.NewGitSig()
|
||||
mergeCtx.committer = mergeCtx.sig
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, mergeCtx.tmpBasePath)
|
||||
if err != nil {
|
||||
defer cancel()
|
||||
return nil, nil, fmt.Errorf("failed to open temp git repo for pr[%d]: %w", mergeCtx.pr.ID, err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
// Determine if we should sign
|
||||
sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch)
|
||||
sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, gitRepo, "HEAD", trackingBranch)
|
||||
if sign {
|
||||
mergeCtx.signKey = key
|
||||
if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package release
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// GenerateReleaseNotesOptions describes how to build release notes content.
|
||||
type GenerateReleaseNotesOptions struct {
|
||||
TagName string
|
||||
TagTarget string
|
||||
PreviousTag string
|
||||
}
|
||||
|
||||
// GenerateReleaseNotes builds the markdown snippet for release notes.
|
||||
func GenerateReleaseNotes(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts GenerateReleaseNotesOptions) (string, error) {
|
||||
headCommit, err := resolveHeadCommit(gitRepo, opts.TagName, opts.TagTarget)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if opts.PreviousTag == "" {
|
||||
// no previous tag, usually due to there is no tag in the repo, use the same content as GitHub
|
||||
content := fmt.Sprintf("**Full Changelog**: %s/commits/tag/%s\n", repo.HTMLURL(ctx), util.PathEscapeSegments(opts.TagName))
|
||||
return content, nil
|
||||
}
|
||||
|
||||
baseCommit, err := gitRepo.GetCommit(opts.PreviousTag)
|
||||
if err != nil {
|
||||
return "", util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_tag_not_found", opts.TagName)
|
||||
}
|
||||
|
||||
commits, err := gitRepo.CommitsBetweenIDs(headCommit.ID.String(), baseCommit.ID.String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("CommitsBetweenIDs: %w", err)
|
||||
}
|
||||
|
||||
prs, err := collectPullRequestsFromCommits(ctx, repo.ID, commits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contributors, newContributors, err := collectContributors(ctx, repo.ID, prs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
content := buildReleaseNotesContent(ctx, repo, opts.TagName, opts.PreviousTag, prs, contributors, newContributors)
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func resolveHeadCommit(gitRepo *git.Repository, tagName, tagTarget string) (*git.Commit, error) {
|
||||
ref := tagName
|
||||
if !gitRepo.IsTagExist(tagName) {
|
||||
ref = tagTarget
|
||||
}
|
||||
|
||||
commit, err := gitRepo.GetCommit(ref)
|
||||
if err != nil {
|
||||
return nil, util.ErrorWrapTranslatable(util.ErrNotExist, "repo.release.generate_notes_target_not_found", ref)
|
||||
}
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
func collectPullRequestsFromCommits(ctx context.Context, repoID int64, commits []*git.Commit) ([]*issues_model.PullRequest, error) {
|
||||
prs := make([]*issues_model.PullRequest, 0, len(commits))
|
||||
|
||||
for _, commit := range commits {
|
||||
pr, err := issues_model.GetPullRequestByMergedCommit(ctx, repoID, commit.ID.String())
|
||||
if err != nil {
|
||||
if issues_model.IsErrPullRequestNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("GetPullRequestByMergedCommit: %w", err)
|
||||
}
|
||||
|
||||
if err = pr.LoadIssue(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadIssue: %w", err)
|
||||
}
|
||||
if err = pr.Issue.LoadAttributes(ctx); err != nil {
|
||||
return nil, fmt.Errorf("LoadIssueAttributes: %w", err)
|
||||
}
|
||||
|
||||
prs = append(prs, pr)
|
||||
}
|
||||
|
||||
slices.SortFunc(prs, func(a, b *issues_model.PullRequest) int {
|
||||
if cmpRes := cmp.Compare(b.MergedUnix, a.MergedUnix); cmpRes != 0 {
|
||||
return cmpRes
|
||||
}
|
||||
return cmp.Compare(b.Issue.Index, a.Issue.Index)
|
||||
})
|
||||
|
||||
return prs, nil
|
||||
}
|
||||
|
||||
func buildReleaseNotesContent(ctx context.Context, repo *repo_model.Repository, tagName, baseRef string, prs []*issues_model.PullRequest, contributors []*user_model.User, newContributors []*issues_model.PullRequest) string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("## What's Changed\n")
|
||||
|
||||
for _, pr := range prs {
|
||||
prURL := pr.Issue.HTMLURL(ctx)
|
||||
builder.WriteString(fmt.Sprintf("* %s in [#%d](%s)\n", pr.Issue.Title, pr.Issue.Index, prURL))
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
|
||||
if len(contributors) > 0 {
|
||||
builder.WriteString("## Contributors\n")
|
||||
for _, contributor := range contributors {
|
||||
builder.WriteString(fmt.Sprintf("* @%s\n", contributor.Name))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(newContributors) > 0 {
|
||||
builder.WriteString("## New Contributors\n")
|
||||
for _, contributor := range newContributors {
|
||||
prURL := contributor.Issue.HTMLURL(ctx)
|
||||
builder.WriteString(fmt.Sprintf("* @%s made their first contribution in [#%d](%s)\n", contributor.Issue.Poster.Name, contributor.Issue.Index, prURL))
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
builder.WriteString("**Full Changelog**: ")
|
||||
compareURL := fmt.Sprintf("%s/compare/%s...%s", repo.HTMLURL(ctx), util.PathEscapeSegments(baseRef), util.PathEscapeSegments(tagName))
|
||||
builder.WriteString(fmt.Sprintf("[%s...%s](%s)", baseRef, tagName, compareURL))
|
||||
builder.WriteByte('\n')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func collectContributors(ctx context.Context, repoID int64, prs []*issues_model.PullRequest) ([]*user_model.User, []*issues_model.PullRequest, error) {
|
||||
contributors := make([]*user_model.User, 0, len(prs))
|
||||
newContributors := make([]*issues_model.PullRequest, 0, len(prs))
|
||||
seenContributors := container.Set[int64]{}
|
||||
seenNew := container.Set[int64]{}
|
||||
|
||||
for _, pr := range prs {
|
||||
poster := pr.Issue.Poster
|
||||
posterID := poster.ID
|
||||
|
||||
if posterID == 0 {
|
||||
// Migrated PRs may not have a linked local user (PosterID == 0). Skip them for now.
|
||||
continue
|
||||
}
|
||||
|
||||
if !seenContributors.Contains(posterID) {
|
||||
contributors = append(contributors, poster)
|
||||
seenContributors.Add(posterID)
|
||||
}
|
||||
|
||||
if seenNew.Contains(posterID) {
|
||||
continue
|
||||
}
|
||||
|
||||
isFirst, err := isFirstContribution(ctx, repoID, posterID, pr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if isFirst {
|
||||
seenNew.Add(posterID)
|
||||
newContributors = append(newContributors, pr)
|
||||
}
|
||||
}
|
||||
|
||||
return contributors, newContributors, nil
|
||||
}
|
||||
|
||||
func isFirstContribution(ctx context.Context, repoID, posterID int64, pr *issues_model.PullRequest) (bool, error) {
|
||||
hasMergedBefore, err := issues_model.HasMergedPullRequestInRepoBefore(ctx, repoID, posterID, pr.MergedUnix, pr.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("check merged PRs for contributor: %w", err)
|
||||
}
|
||||
return !hasMergedBefore, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package release
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGenerateReleaseNotes(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("ChangeLogsWithPRs", func(t *testing.T) {
|
||||
mergedCommit := "90c1019714259b24fb81711d4416ac0f18667dfa"
|
||||
createMergedPullRequest(t, repo, mergedCommit, 5)
|
||||
|
||||
content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
|
||||
TagName: "v1.2.0",
|
||||
TagTarget: "DefaultBranch",
|
||||
PreviousTag: "v1.1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `## What's Changed
|
||||
* Release notes test pull request in [#6](https://try.gitea.io/user2/repo1/pulls/6)
|
||||
|
||||
## Contributors
|
||||
* @user5
|
||||
|
||||
## New Contributors
|
||||
* @user5 made their first contribution in [#6](https://try.gitea.io/user2/repo1/pulls/6)
|
||||
|
||||
**Full Changelog**: [v1.1...v1.2.0](https://try.gitea.io/user2/repo1/compare/v1.1...v1.2.0)
|
||||
`, content)
|
||||
})
|
||||
|
||||
t.Run("NoPreviousTag", func(t *testing.T) {
|
||||
content, err := GenerateReleaseNotes(t.Context(), repo, gitRepo, GenerateReleaseNotesOptions{
|
||||
TagName: "v1.2.0",
|
||||
TagTarget: "DefaultBranch",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "**Full Changelog**: https://try.gitea.io/user2/repo1/commits/tag/v1.2.0\n", content)
|
||||
})
|
||||
}
|
||||
|
||||
func createMergedPullRequest(t *testing.T, repo *repo_model.Repository, mergeCommit string, posterID int64) *issues_model.PullRequest {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: posterID})
|
||||
|
||||
issue := &issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
Poster: user,
|
||||
PosterID: user.ID,
|
||||
Title: "Release notes test pull request",
|
||||
Content: "content",
|
||||
}
|
||||
|
||||
pr := &issues_model.PullRequest{
|
||||
HeadRepoID: repo.ID,
|
||||
BaseRepoID: repo.ID,
|
||||
HeadBranch: repo.DefaultBranch,
|
||||
BaseBranch: repo.DefaultBranch,
|
||||
Status: issues_model.PullRequestStatusMergeable,
|
||||
Flow: issues_model.PullRequestFlowGithub,
|
||||
}
|
||||
|
||||
require.NoError(t, issues_model.NewPullRequest(t.Context(), repo, issue, nil, nil, pr))
|
||||
|
||||
pr.HasMerged = true
|
||||
pr.MergedCommitID = mergeCommit
|
||||
pr.MergedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(t.Context()).
|
||||
ID(pr.ID).
|
||||
Cols("has_merged", "merged_commit_id", "merged_unix").
|
||||
Update(pr)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, pr.LoadIssue(t.Context()))
|
||||
require.NoError(t, pr.Issue.LoadAttributes(t.Context()))
|
||||
return pr
|
||||
}
|
||||
@@ -526,7 +526,6 @@ func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git
|
||||
}
|
||||
|
||||
pushOpts := git.PushOptions{
|
||||
Remote: repo.RepoPath(),
|
||||
Branch: fmt.Sprintf("%s:%s%s", newCommit.ID.String(), git.BranchPrefix, branchName),
|
||||
Env: repo_module.PushingEnvironment(doer, repo),
|
||||
Force: isForcePush || force,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/pull"
|
||||
@@ -35,7 +36,13 @@ func (err ErrCommitIDDoesNotMatch) Error() string {
|
||||
|
||||
// CherryPick cherry-picks or reverts a commit to the given repository
|
||||
func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
|
||||
if err := opts.Validate(ctx, repo, doer); err != nil {
|
||||
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
if err := opts.Validate(ctx, repo, gitRepo, doer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message := strings.TrimSpace(opts.Message)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
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"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -52,7 +53,7 @@ type ApplyDiffPatchOptions struct {
|
||||
}
|
||||
|
||||
// Validate validates the provided options
|
||||
func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error {
|
||||
func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doer *user_model.User) error {
|
||||
// If no branch name is set, assume master
|
||||
if opts.OldBranch == "" {
|
||||
opts.OldBranch = repo.DefaultBranch
|
||||
@@ -95,7 +96,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode
|
||||
}
|
||||
}
|
||||
if protectedBranch != nil && protectedBranch.RequireSignedCommits {
|
||||
_, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), opts.OldBranch)
|
||||
_, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, gitRepo, opts.OldBranch)
|
||||
if err != nil {
|
||||
if !asymkey_service.IsErrWontSign(err) {
|
||||
return err
|
||||
@@ -116,7 +117,13 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := opts.Validate(ctx, repo, doer); err != nil {
|
||||
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
if err := opts.Validate(ctx, repo, gitRepo, doer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -303,9 +303,9 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
|
||||
var key *git.SigningKey
|
||||
var signer *git.Signature
|
||||
if opts.ParentCommitID != "" {
|
||||
sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID)
|
||||
sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, opts.DoerUser, t.gitRepo, opts.ParentCommitID)
|
||||
} else {
|
||||
sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser)
|
||||
sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, opts.DoerUser)
|
||||
}
|
||||
if sign {
|
||||
if key.Format != "" {
|
||||
|
||||
@@ -167,7 +167,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if err := VerifyBranchProtection(ctx, repo, doer, opts.OldBranch, treePaths); err != nil {
|
||||
} else if err := VerifyBranchProtection(ctx, repo, gitRepo, doer, opts.OldBranch, treePaths); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository,
|
||||
}
|
||||
|
||||
// VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch
|
||||
func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, branchName string, treePaths []string) error {
|
||||
func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doer *user_model.User, branchName string, treePaths []string) error {
|
||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branchName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -686,7 +686,7 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, do
|
||||
}
|
||||
}
|
||||
if protectedBranch.RequireSignedCommits {
|
||||
_, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), branchName)
|
||||
_, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, gitRepo, branchName)
|
||||
if err != nil {
|
||||
if !asymkey_service.IsErrWontSign(err) {
|
||||
return err
|
||||
|
||||
@@ -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, "/"), "/")))
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -41,7 +41,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
|
||||
cmd := gitcmd.NewCommand("commit", "--message=Initial commit").
|
||||
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
|
||||
|
||||
sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u)
|
||||
sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, u)
|
||||
if sign {
|
||||
if key.Format != "" {
|
||||
cmd.AddConfig("gpg.format", key.Format)
|
||||
|
||||
@@ -257,7 +257,7 @@ func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error
|
||||
|
||||
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
|
||||
if !isPublic && isExist {
|
||||
if err = gitrepo.RemoveRepoFile(ctx, repo, daemonExportFile); err != nil {
|
||||
if err = gitrepo.RemoveRepoFileOrDir(ctx, repo, daemonExportFile); err != nil {
|
||||
log.Error("Failed to remove %s: %v", daemonExportFile, err)
|
||||
}
|
||||
} else if isPublic && !isExist {
|
||||
|
||||
@@ -72,22 +72,22 @@ func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
|
||||
|
||||
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text string
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
var authorName string
|
||||
if commit.Author != nil {
|
||||
authorName = " - " + commit.Author.Name
|
||||
}
|
||||
text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n")) + authorName
|
||||
text.WriteString(fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n")) + authorName)
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text += "\r\n"
|
||||
text.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
return createDingtalkPayload(title, text, linkText, titleLink), nil
|
||||
return createDingtalkPayload(title, text.String(), linkText, titleLink), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
|
||||
@@ -159,7 +159,7 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
|
||||
|
||||
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text string
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
// limit the commit message display to just the summary, otherwise it would be hard to read
|
||||
@@ -169,14 +169,14 @@ func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
|
||||
if utf8.RuneCountInString(message) > 50 {
|
||||
message = fmt.Sprintf("%.47s...", message)
|
||||
}
|
||||
text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name)
|
||||
text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name))
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text += "\n"
|
||||
text.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return d.createPayload(p.Sender, title, text, titleLink, greenColor), nil
|
||||
return d.createPayload(p.Sender, title, text.String(), titleLink, greenColor), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
|
||||
@@ -76,22 +76,23 @@ func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
|
||||
commitDesc string
|
||||
)
|
||||
|
||||
text := fmt.Sprintf("[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc)
|
||||
var text strings.Builder
|
||||
text.WriteString(fmt.Sprintf("[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc))
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
var authorName string
|
||||
if commit.Author != nil {
|
||||
authorName = " - " + commit.Author.Name
|
||||
}
|
||||
text += fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n")) + authorName
|
||||
text.WriteString(fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n")) + authorName)
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text += "\r\n"
|
||||
text.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
return newFeishuTextPayload(text), nil
|
||||
return newFeishuTextPayload(text.String()), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
|
||||
@@ -173,18 +173,19 @@ func (m matrixConvertor) Push(p *api.PushPayload) (MatrixPayload, error) {
|
||||
|
||||
repoLink := htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
|
||||
branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
|
||||
text := fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink)
|
||||
var text strings.Builder
|
||||
text.WriteString(fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink))
|
||||
|
||||
// for each commit, generate a new line text
|
||||
for i, commit := range p.Commits {
|
||||
text += fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
|
||||
text.WriteString(fmt.Sprintf("%s: %s - %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name))
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text += "<br>"
|
||||
text.WriteString("<br>")
|
||||
}
|
||||
}
|
||||
|
||||
return m.newPayload(text, p.Commits...)
|
||||
return m.newPayload(text.String(), p.Commits...)
|
||||
}
|
||||
|
||||
// PullRequest implements payloadConvertor PullRequest method
|
||||
|
||||
@@ -131,14 +131,14 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
|
||||
|
||||
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text string
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
|
||||
text.WriteString(fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
|
||||
strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name))
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text += "\n\n"
|
||||
text.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
|
||||
p.Repo,
|
||||
p.Sender,
|
||||
title,
|
||||
text,
|
||||
text.String(),
|
||||
titleLink,
|
||||
greenColor,
|
||||
&MSTeamsFact{"Commit count:", strconv.Itoa(p.TotalCommits)},
|
||||
|
||||
@@ -208,13 +208,13 @@ func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
|
||||
branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
|
||||
text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName)
|
||||
|
||||
var attachmentText string
|
||||
var attachmentText strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
attachmentText += fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name))
|
||||
attachmentText.WriteString(fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name)))
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
attachmentText += "\n"
|
||||
attachmentText.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
|
||||
Color: s.Color,
|
||||
Title: p.Repo.HTMLURL,
|
||||
TitleLink: p.Repo.HTMLURL,
|
||||
Text: attachmentText,
|
||||
Text: attachmentText.String(),
|
||||
}}), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -94,14 +94,14 @@ func (t telegramConvertor) Push(p *api.PushPayload) (TelegramPayload, error) {
|
||||
}
|
||||
title := fmt.Sprintf(`[%s:%s] %s`, htmlLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName), htmlLinkFormatter(titleLink, branchName), html.EscapeString(commitDesc))
|
||||
|
||||
var htmlCommits string
|
||||
var htmlCommits strings.Builder
|
||||
for _, commit := range p.Commits {
|
||||
htmlCommits += fmt.Sprintf("\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n")))
|
||||
htmlCommits.WriteString(fmt.Sprintf("\n[%s] %s", htmlLinkFormatter(commit.URL, commit.ID[:7]), html.EscapeString(strings.TrimRight(commit.Message, "\r\n"))))
|
||||
if commit.Author != nil {
|
||||
htmlCommits += " - " + html.EscapeString(commit.Author.Name)
|
||||
htmlCommits.WriteString(" - " + html.EscapeString(commit.Author.Name))
|
||||
}
|
||||
}
|
||||
return createTelegramPayloadHTML(title + htmlCommits), nil
|
||||
return createTelegramPayloadHTML(title + htmlCommits.String()), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
|
||||
@@ -77,7 +77,7 @@ func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error
|
||||
|
||||
title := fmt.Sprintf("# %s:%s <font color=\"warning\"> %s </font>", p.Repo.FullName, branchName, commitDesc)
|
||||
|
||||
var text string
|
||||
var text strings.Builder
|
||||
// for each commit, generate attachment text
|
||||
for i, commit := range p.Commits {
|
||||
var authorName string
|
||||
@@ -86,15 +86,15 @@ func (wc wechatworkConvertor) Push(p *api.PushPayload) (WechatworkPayload, error
|
||||
}
|
||||
|
||||
message := strings.ReplaceAll(commit.Message, "\n\n", "\r\n")
|
||||
text += fmt.Sprintf(" > [%s](%s) \r\n ><font color=\"info\">%s</font> \n ><font color=\"warning\">%s</font>", commit.ID[:7], commit.URL,
|
||||
message, authorName)
|
||||
text.WriteString(fmt.Sprintf(" > [%s](%s) \r\n ><font color=\"info\">%s</font> \n ><font color=\"warning\">%s</font>", commit.ID[:7], commit.URL,
|
||||
message, authorName))
|
||||
|
||||
// add linebreak to each commit but the last
|
||||
if i < len(p.Commits)-1 {
|
||||
text += "\n"
|
||||
text.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return newWechatworkMarkdownPayload(title + "\r\n\r\n" + text), nil
|
||||
return newWechatworkMarkdownPayload(title + "\r\n\r\n" + text.String()), nil
|
||||
}
|
||||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
|
||||
+15
-3
@@ -193,7 +193,13 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
|
||||
committer := doer.NewGitSig()
|
||||
|
||||
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
|
||||
originalGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo.WikiStorageRepo())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open wiki repository: %w", err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, originalGitRepo, doer)
|
||||
if sign {
|
||||
commitTreeOpts.Key = signingKey
|
||||
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||
@@ -212,7 +218,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gitrepo.PushFromLocal(gitRepo.Ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{
|
||||
if err := gitrepo.PushFromLocal(ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{
|
||||
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
|
||||
Env: repo_module.FullPushingEnvironment(
|
||||
doer,
|
||||
@@ -315,7 +321,13 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
|
||||
committer := doer.NewGitSig()
|
||||
|
||||
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
|
||||
originalGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo.WikiStorageRepo())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open wiki repository: %w", err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, originalGitRepo, doer)
|
||||
if sign {
|
||||
commitTreeOpts.Key = signingKey
|
||||
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||
|
||||
Reference in new issue
Block a user