[gitea] week 2025-02 cherry pick (gitea/main -> forgejo) (#6471)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6471
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2025-01-07 06:17:49 +00:00
commit 4261301dfb
34 changed files with 341 additions and 127 deletions

View file

@ -69,6 +69,10 @@ var microcmdUserCreate = &cli.Command{
}
func runCreateUser(c *cli.Context) error {
// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings()
if err := argsSet(c, "email"); err != nil {
return err
}

View file

@ -206,6 +206,7 @@ func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmd
app.Commands = append(app.Commands, subCmdWithConfig...)
app.Commands = append(app.Commands, subCmdStandalone...)
setting.InitGiteaEnvVars()
return app
}

View file

@ -6,7 +6,6 @@ package cmd
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
@ -114,37 +113,17 @@ func TestCliCmd(t *testing.T) {
_, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
})
var envBackup []string
for _, s := range os.Environ() {
if strings.HasPrefix(s, "GITEA_") && strings.Contains(s, "=") {
envBackup = append(envBackup, s)
}
}
clearGiteaEnv := func() {
for _, s := range os.Environ() {
if strings.HasPrefix(s, "GITEA_") {
_ = os.Unsetenv(s)
}
}
}
defer func() {
clearGiteaEnv()
for _, s := range envBackup {
k, v, _ := strings.Cut(s, "=")
_ = os.Setenv(k, v)
}
}()
for _, c := range cases {
clearGiteaEnv()
t.Run(c.cmd, func(t *testing.T) {
for k, v := range c.env {
_ = os.Setenv(k, v)
t.Setenv(k, v)
}
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
r, err := runTestApp(app, args...)
require.NoError(t, err, c.cmd)
assert.NotEmpty(t, c.exp, c.cmd)
assert.Contains(t, r.Stdout, c.exp, c.cmd)
})
}
}

View file

@ -12,6 +12,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
_ "net/http/pprof" // Used for debugging if enabled and a web server is running
@ -115,6 +116,16 @@ func showWebStartupMessage(msg string) {
log.Info("* CustomPath: %s", setting.CustomPath)
log.Info("* ConfigFile: %s", setting.CustomConf)
log.Info("%s", msg) // show startup message
if setting.CORSConfig.Enabled {
log.Info("CORS Service Enabled")
}
if setting.DefaultUILocation != time.Local {
log.Info("Default UI Location is %v", setting.DefaultUILocation.String())
}
if setting.MailService != nil {
log.Info("Mail Service Enabled: RegisterEmailConfirm=%v, Service.EnableNotifyMail=%v", setting.Service.RegisterEmailConfirm, setting.Service.EnableNotifyMail)
}
}
func serveInstall(ctx *cli.Context) error {

View file

@ -194,6 +194,20 @@ func (t CommentType) HasMailReplySupport() bool {
return false
}
func (t CommentType) CountedAsConversation() bool {
for _, ct := range ConversationCountedCommentType() {
if t == ct {
return true
}
}
return false
}
// ConversationCountedCommentType returns the comment types that are counted as a conversation
func ConversationCountedCommentType() []CommentType {
return []CommentType{CommentTypeComment, CommentTypeReview}
}
// RoleInRepo presents the user's participation in the repo
type RoleInRepo string
@ -887,7 +901,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
}
fallthrough
case CommentTypeComment:
if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
if err := UpdateIssueNumComments(ctx, opts.Issue.ID); err != nil {
return err
}
fallthrough
@ -1182,8 +1196,8 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
return err
}
if comment.Type == CommentTypeComment {
if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil {
if comment.Type.CountedAsConversation() {
if err := UpdateIssueNumComments(ctx, comment.IssueID); err != nil {
return err
}
}
@ -1300,6 +1314,21 @@ func (c *Comment) HasOriginalAuthor() bool {
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
}
func UpdateIssueNumCommentsBuilder(issueID int64) *builder.Builder {
subQuery := builder.Select("COUNT(*)").From("`comment`").Where(
builder.Eq{"issue_id": issueID}.And(
builder.In("`type`", ConversationCountedCommentType()),
))
return builder.Update(builder.Eq{"num_comments": subQuery}).
From("`issue`").Where(builder.Eq{"id": issueID})
}
func UpdateIssueNumComments(ctx context.Context, issueID int64) error {
_, err := db.GetEngine(ctx).Exec(UpdateIssueNumCommentsBuilder(issueID))
return err
}
// InsertIssueComments inserts many comments of issues.
func InsertIssueComments(ctx context.Context, comments []*Comment) error {
if len(comments) == 0 {
@ -1332,8 +1361,7 @@ func InsertIssueComments(ctx context.Context, comments []*Comment) error {
}
for _, issueID := range issueIDs {
if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?",
issueID, CommentTypeComment, issueID); err != nil {
if err := UpdateIssueNumComments(ctx, issueID); err != nil {
return err
}
}

View file

@ -125,3 +125,12 @@ func TestUpdateCommentsMigrationsByType(t *testing.T) {
assert.Empty(t, comment.OriginalAuthorID)
assert.EqualValues(t, 513, comment.PosterID)
}
func Test_UpdateIssueNumComments(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
require.NoError(t, issues_model.UpdateIssueNumComments(db.DefaultContext, issue2.ID))
issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
assert.EqualValues(t, 1, issue2.NumComments)
}

View file

@ -614,6 +614,10 @@ func InsertReviews(ctx context.Context, reviews []*Review) error {
return err
}
}
if err := UpdateIssueNumComments(ctx, review.IssueID); err != nil {
return err
}
}
return committer.Commit()

View file

@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) {
return err
}
func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint
return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID)
}
func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint
return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID)
}
// Link returns the project's relative URL.
func (p *Project) Link(ctx context.Context) string {
if p.OwnerID > 0 {
@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string {
log.Error("LoadOwner: %v", err)
return ""
}
return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID)
return ProjectLinkForOrg(p.Owner, p.ID)
}
if p.RepoID > 0 {
err := p.LoadRepo(ctx)
@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string {
log.Error("LoadRepo: %v", err)
return ""
}
return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID)
return ProjectLinkForRepo(p.Repo, p.ID)
}
return ""
}

View file

@ -19,6 +19,8 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"xorm.io/builder"
)
// Init initialize model
@ -27,7 +29,7 @@ func Init(ctx context.Context) error {
}
type repoChecker struct {
querySQL func(ctx context.Context) ([]map[string][]byte, error)
querySQL func(ctx context.Context) ([]int64, error)
correctSQL func(ctx context.Context, id int64) error
desc string
}
@ -38,8 +40,7 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) {
log.Error("Select %s: %v", checker.desc, err)
return
}
for _, result := range results {
id, _ := strconv.ParseInt(string(result["id"]), 10, 64)
for _, id := range results {
select {
case <-ctx.Done():
log.Warn("CheckRepoStats: Cancelled before checking %s for with id=%d", checker.desc, id)
@ -54,21 +55,23 @@ func repoStatsCheck(ctx context.Context, checker *repoChecker) {
}
}
func StatsCorrectSQL(ctx context.Context, sql string, id int64) error {
_, err := db.GetEngine(ctx).Exec(sql, id, id)
func StatsCorrectSQL(ctx context.Context, sql any, ids ...any) error {
args := []any{sql}
args = append(args, ids...)
_, err := db.GetEngine(ctx).Exec(args...)
return err
}
func repoStatsCorrectNumWatches(ctx context.Context, id int64) error {
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id)
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", id, id)
}
func repoStatsCorrectNumStars(ctx context.Context, id int64) error {
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id)
return StatsCorrectSQL(ctx, "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?", id, id)
}
func labelStatsCorrectNumIssues(ctx context.Context, id int64) error {
return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id)
return StatsCorrectSQL(ctx, "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?", id, id)
}
func labelStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
@ -105,11 +108,11 @@ func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error {
}
func userStatsCorrectNumRepos(ctx context.Context, id int64) error {
return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id)
return StatsCorrectSQL(ctx, "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", id, id)
}
func repoStatsCorrectIssueNumComments(ctx context.Context, id int64) error {
return StatsCorrectSQL(ctx, "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", id)
return StatsCorrectSQL(ctx, issues_model.UpdateIssueNumCommentsBuilder(id))
}
func repoStatsCorrectNumIssues(ctx context.Context, id int64) error {
@ -128,9 +131,12 @@ func repoStatsCorrectNumClosedPulls(ctx context.Context, id int64) error {
return repo_model.UpdateRepoIssueNumbers(ctx, id, true, true)
}
func statsQuery(args ...any) func(context.Context) ([]map[string][]byte, error) {
return func(ctx context.Context) ([]map[string][]byte, error) {
return db.GetEngine(ctx).Query(args...)
// statsQuery returns a function that queries the database for a list of IDs
// sql could be a string or a *builder.Builder
func statsQuery(sql any, args ...any) func(context.Context) ([]int64, error) {
return func(ctx context.Context) ([]int64, error) {
var ids []int64
return ids, db.GetEngine(ctx).SQL(sql, args...).Find(&ids)
}
}
@ -201,7 +207,16 @@ func CheckRepoStats(ctx context.Context) error {
},
// Issue.NumComments
{
statsQuery("SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)"),
statsQuery(builder.Select("`issue`.id").From("`issue`").Where(
builder.Neq{
"`issue`.num_comments": builder.Select("COUNT(*)").From("`comment`").Where(
builder.Expr("issue_id = `issue`.id").And(
builder.In("type", issues_model.ConversationCountedCommentType()),
),
),
},
),
),
repoStatsCorrectIssueNumComments,
"issue count 'num_comments'",
},

View file

@ -7,8 +7,10 @@ import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -22,3 +24,16 @@ func TestDoctorUserStarNum(t *testing.T) {
require.NoError(t, DoctorUserStarNum(db.DefaultContext))
}
func Test_repoStatsCorrectIssueNumComments(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
assert.NotNil(t, issue2)
assert.EqualValues(t, 0, issue2.NumComments) // the fixture data is wrong, but we don't fix it here
require.NoError(t, repoStatsCorrectIssueNumComments(db.DefaultContext, 2))
// reload the issue
issue2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
assert.EqualValues(t, 1, issue2.NumComments)
}

View file

@ -59,6 +59,13 @@ func InitSettings() {
_ = hash.Register("dummy", hash.NewDummyHasher)
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
setting.InitGiteaEnvVars()
// Avoid loading the git's system config.
// On macOS, system config sets the osxkeychain credential helper, which will cause tests to freeze with a dialog.
// But we do not set it in production at the moment, because it might be a "breaking" change,
// more details are in "modules/git.commonBaseEnvs".
_ = os.Setenv("GIT_CONFIG_NOSYSTEM", "true")
}
// TestOptions represents test options

View file

@ -176,3 +176,14 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error
return filelist, err
}
// GetTreePathLatestCommitID returns the latest commit of a tree path
func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) {
stdout, _, err := NewCommand(repo.Ctx, "rev-list", "-1").
AddDynamicArguments(refName).AddDashesAndList(treePath).
RunStdString(&RunOpts{Dir: repo.Path})
if err != nil {
return nil, err
}
return repo.GetCommit(strings.TrimSpace(stdout))
}

View file

@ -26,3 +26,18 @@ func TestSubTree_Issue29101(t *testing.T) {
assert.True(t, IsErrNotExist(err))
}
}
func Test_GetTreePathLatestCommit(t *testing.T) {
repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo6_blame"))
require.NoError(t, err)
defer repo.Close()
commitID, err := repo.GetBranchCommitID("master")
require.NoError(t, err)
assert.EqualValues(t, "544d8f7a3b15927cddf2299b4b562d6ebd71b6a7", commitID)
commit, err := repo.GetTreePathLatestCommit("master", "blame.txt")
require.NoError(t, err)
assert.NotNil(t, commit)
assert.EqualValues(t, "45fb6cbc12f970b04eacd5cd4165edd11c8d7376", commit.ID.String())
}

View file

@ -168,3 +168,22 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) {
}
return changed
}
// InitGiteaEnvVars initializes the environment variables for gitea
func InitGiteaEnvVars() {
// Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want,
// but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users,
// so at the moment we could still keep "unsetting the unnecessary environments"
// HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig".
// But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist,
// then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set.
_ = os.Unsetenv("XDG_CONFIG_HOME")
_ = os.Unsetenv("GIT_AUTHOR_NAME")
_ = os.Unsetenv("GIT_AUTHOR_EMAIL")
_ = os.Unsetenv("GIT_AUTHOR_DATE")
_ = os.Unsetenv("GIT_COMMITTER_NAME")
_ = os.Unsetenv("GIT_COMMITTER_EMAIL")
_ = os.Unsetenv("GIT_COMMITTER_DATE")
}

View file

@ -5,8 +5,6 @@ package setting
import (
"time"
"code.gitea.io/gitea/modules/log"
)
// CORSConfig defines CORS settings
@ -28,7 +26,4 @@ var CORSConfig = struct {
func loadCorsFrom(rootCfg ConfigProvider) {
mustMapSetting(rootCfg, "cors", &CORSConfig)
if CORSConfig.Enabled {
log.Info("CORS Service Enabled")
}
}

View file

@ -109,7 +109,7 @@ func IndexerGlobFromString(globstr string) []Glob {
expr = strings.TrimSpace(expr)
if expr != "" {
if g, err := glob.Compile(expr, '.', '/'); err != nil {
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
log.Warn("Invalid glob expression '%s' (skipped): %v", expr, err)
} else {
extarr = append(extarr, Glob{glob: g, pattern: expr})
}

View file

@ -263,8 +263,6 @@ func loadMailerFrom(rootCfg ConfigProvider) {
MailService.OverrideEnvelopeFrom = true
MailService.EnvelopeFrom = parsed.Address
}
log.Info("Mail Service Enabled")
}
func loadRegisterMailFrom(rootCfg ConfigProvider) {
@ -275,7 +273,6 @@ func loadRegisterMailFrom(rootCfg ConfigProvider) {
return
}
Service.RegisterEmailConfirm = true
log.Info("Register Mail Service Enabled")
}
func loadNotifyMailFrom(rootCfg ConfigProvider) {
@ -286,7 +283,6 @@ func loadNotifyMailFrom(rootCfg ConfigProvider) {
return
}
Service.EnableNotifyMail = true
log.Info("Notify Mail Service Enabled")
}
func tryResolveAddr(addr string) []net.IPAddr {

View file

@ -73,6 +73,4 @@ func loadSessionFrom(rootCfg ConfigProvider) {
SessionConfig.ProviderConfig = string(shadowConfig)
SessionConfig.OriginalProvider = SessionConfig.Provider
SessionConfig.Provider = "VirtualSession"
log.Info("Session Service Enabled")
}

View file

@ -20,7 +20,6 @@ func loadTimeFrom(rootCfg ConfigProvider) {
if err != nil {
log.Fatal("Load time zone failed: %v", err)
}
log.Info("Default UI Location is %v", zone)
}
if DefaultUILocation == nil {
DefaultUILocation = time.Local

View file

@ -3643,6 +3643,7 @@ versions = Versions
versions.view_all = View all
dependency.id = ID
dependency.version = Version
search_in_external_registry = Search in %s
alpine.registry = Setup this registry by adding the url in your <code>/etc/apk/repositories</code> file:
alpine.registry.key = Download the registry public RSA key into the <code>/etc/apk/keys/</code> folder to verify the index signature:
alpine.registry.info = Choose $branch and $repository from the list below.

1
release-notes/6471.md Normal file
View file

@ -0,0 +1 @@
feat: [commit](https://codeberg.org/forgejo/forgejo/commit/76a85d26c8576fc410dc6494f2907ffc2b353c39) Use `Project-URL` metadata field to get a PyPI package's homepage URL

View file

@ -10,6 +10,7 @@ import (
"regexp"
"sort"
"strings"
"unicode"
packages_model "code.gitea.io/gitea/models/packages"
packages_module "code.gitea.io/gitea/modules/packages"
@ -139,9 +140,30 @@ func UploadPackageFile(ctx *context.Context) {
return
}
projectURL := ctx.Req.FormValue("home_page")
if !validation.IsValidURL(projectURL) {
projectURL = ""
// Ensure ctx.Req.Form exists.
_ = ctx.Req.ParseForm()
var homepageURL string
projectURLs := ctx.Req.Form["project_urls"]
for _, purl := range projectURLs {
label, url, found := strings.Cut(purl, ",")
if !found {
continue
}
if normalizeLabel(label) != "homepage" {
continue
}
homepageURL = strings.TrimSpace(url)
break
}
if len(homepageURL) == 0 {
// TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec.
homepageURL = ctx.Req.FormValue("home_page")
}
if !validation.IsValidURL(homepageURL) {
homepageURL = ""
}
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
@ -160,7 +182,7 @@ func UploadPackageFile(ctx *context.Context) {
Description: ctx.Req.FormValue("description"),
LongDescription: ctx.Req.FormValue("long_description"),
Summary: ctx.Req.FormValue("summary"),
ProjectURL: projectURL,
ProjectURL: homepageURL,
License: ctx.Req.FormValue("license"),
RequiresPython: ctx.Req.FormValue("requires_python"),
},
@ -189,6 +211,23 @@ func UploadPackageFile(ctx *context.Context) {
ctx.Status(http.StatusCreated)
}
// Normalizes a Project-URL label.
// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization.
func normalizeLabel(label string) string {
var builder strings.Builder
// "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result
// to lowercase."
for _, r := range label {
if unicode.IsPunct(r) || unicode.IsSpace(r) {
continue
}
builder.WriteRune(unicode.ToLower(r))
}
return builder.String()
}
func isValidNameAndVersion(packageName, packageVersion string) bool {
return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
}

View file

@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) {
assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa"))
assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta"))
}
func TestNormalizeLabel(t *testing.T) {
// Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization.
assert.Equal(t, "homepage", normalizeLabel("Homepage"))
assert.Equal(t, "homepage", normalizeLabel("Home-page"))
assert.Equal(t, "homepage", normalizeLabel("Home page"))
assert.Equal(t, "changelog", normalizeLabel("Change_Log"))
assert.Equal(t, "whatsnew", normalizeLabel("What's New?"))
assert.Equal(t, "github", normalizeLabel("github"))
}

View file

@ -11,7 +11,6 @@ import (
"fmt"
"io"
"net/http"
"path"
"strings"
"time"
@ -247,19 +246,14 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEn
return nil, nil, nil
}
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
ctx.Error(http.StatusInternalServerError, "GetTreePathLatestCommit", err)
return nil, nil, nil
}
when := &latestCommit.Committer.When
if len(info) == 1 {
// Not Modified
lastModified = &info[0].Commit.Committer.When
}
blob = entry.Blob()
return blob, entry, lastModified
return entry.Blob(), entry, when
}
// GetArchive get archive of a repository

View file

@ -209,7 +209,7 @@ func ChangeProjectStatus(ctx *context.Context) {
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
return
}
ctx.JSONRedirect(fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), id))
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.ContextUser, id))
}
// DeleteProject delete a project
@ -259,7 +259,7 @@ func RenderEditProject(ctx *context.Context) {
ctx.Data["redirect"] = ctx.FormString("redirect")
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
ctx.Data["card_type"] = p.CardType
ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), p.ID)
ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, p.ID)
ctx.HTML(http.StatusOK, tplProjectsNew)
}
@ -273,7 +273,7 @@ func EditProjectPost(ctx *context.Context) {
ctx.Data["PageIsViewProjects"] = true
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CancelLink"] = fmt.Sprintf("%s/-/projects/%d", ctx.ContextUser.HomeLink(), projectID)
ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID)
shared_user.RenderUserHeader(ctx)

View file

@ -5,7 +5,6 @@
package repo
import (
"path"
"time"
git_model "code.gitea.io/gitea/models/git"
@ -82,7 +81,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
}
func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.Time) {
func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
@ -98,19 +97,14 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified *time.T
return nil, nil
}
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
latestCommit, err := ctx.Repo.GitRepo.GetTreePathLatestCommit(ctx.Repo.Commit.ID.String(), ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("GetCommitsInfo", err)
ctx.ServerError("GetTreePathLatestCommit", err)
return nil, nil
}
lastModified := &latestCommit.Committer.When
if len(info) == 1 {
// Not Modified
lastModified = &info[0].Commit.Committer.When
}
blob = entry.Blob()
return blob, lastModified
return entry.Blob(), lastModified
}
// SingleDownload download a file by repos path

View file

@ -1291,10 +1291,17 @@ func NewIssuePost(ctx *context.Context) {
log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
if ctx.FormString("redirect_after_creation") == "project" && projectID > 0 {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/projects/" + strconv.FormatInt(projectID, 10))
project, err := project_model.GetProjectByID(ctx, projectID)
if err == nil {
if project.Type == project_model.TypeOrganization {
ctx.JSONRedirect(project_model.ProjectLinkForOrg(ctx.Repo.Owner, project.ID))
} else {
ctx.JSONRedirect(issue.Link())
ctx.JSONRedirect(project_model.ProjectLinkForRepo(repo, project.ID))
}
return
}
}
ctx.JSONRedirect(issue.Link())
}
// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue

View file

@ -183,7 +183,7 @@ func ChangeProjectStatus(ctx *context.Context) {
ctx.NotFoundOrServerError("ChangeProjectStatusByRepoIDAndID", project_model.IsErrProjectNotExist, err)
return
}
ctx.JSONRedirect(fmt.Sprintf("%s/projects/%d", ctx.Repo.RepoLink, id))
ctx.JSONRedirect(project_model.ProjectLinkForRepo(ctx.Repo.Repository, id))
}
// DeleteProject delete a project
@ -237,7 +237,7 @@ func RenderEditProject(ctx *context.Context) {
ctx.Data["content"] = p.Description
ctx.Data["card_type"] = p.CardType
ctx.Data["redirect"] = ctx.FormString("redirect")
ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), p.ID)
ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, p.ID)
ctx.HTML(http.StatusOK, tplProjectsNew)
}
@ -251,7 +251,7 @@ func EditProjectPost(ctx *context.Context) {
ctx.Data["PageIsEditProjects"] = true
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CancelLink"] = fmt.Sprintf("%s/projects/%d", ctx.Repo.Repository.Link(), projectID)
ctx.Data["CancelLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplProjectsNew)

View file

@ -5,6 +5,7 @@ package pull
import (
"fmt"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
@ -66,7 +67,10 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() {
// add trailer
message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String())
if !strings.Contains(message, fmt.Sprintf("Co-authored-by: %s", sig.String())) {
message += fmt.Sprintf("\nCo-authored-by: %s", sig.String())
}
message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String())
}
cmdCommit := git.NewCommand(ctx, "commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).

View file

@ -10,9 +10,12 @@ import (
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/convert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -98,3 +101,11 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) {
})
}
}
func TestWebhookUserMail(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
setting.Service.NoReplyAddress = "no-reply.com"
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email)
assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email)
}

View file

@ -35,11 +35,12 @@
</tr>
</thead>
<tbody>
{{$tooltipSearchInNuget := ctx.Locale.Tr "packages.search_in_external_registry" "nuget.org"}}
{{range $framework, $dependencies := .PackageDescriptor.Metadata.Dependencies}}
{{range $dependencies}}
<tr>
<td>{{.ID}}</td>
<td>{{.Version}}</td>
<td>{{.ID}} <a target="_blank" rel="noreferrer" href="https://www.nuget.org/packages/{{.ID}}" data-tooltip-content="{{$tooltipSearchInNuget}}">{{svg "octicon-link-external"}}</a></td>
<td>{{.Version}} <a target="_blank" rel="noreferrer" href="https://www.nuget.org/packages/{{.ID}}/{{.Version}}" data-tooltip-content="{{$tooltipSearchInNuget}}">{{svg "octicon-link-external"}}</a></td>
<td>{{$framework}}</td>
</tr>
{{end}}

View file

@ -40,13 +40,6 @@ func TestMain(m *testing.M) {
initChangedFiles()
testE2eWebRoutes = routers.NormalRoutes()
os.Unsetenv("GIT_AUTHOR_NAME")
os.Unsetenv("GIT_AUTHOR_EMAIL")
os.Unsetenv("GIT_AUTHOR_DATE")
os.Unsetenv("GIT_COMMITTER_NAME")
os.Unsetenv("GIT_COMMITTER_EMAIL")
os.Unsetenv("GIT_COMMITTER_DATE")
err := unittest.InitFixtures(
unittest.FixturesOptions{
Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),

View file

@ -33,15 +33,16 @@ func TestPackagePyPI(t *testing.T) {
packageVersion := "1!1.0.1+r1234"
packageAuthor := "KN4CK3R"
packageDescription := "Test Description"
projectURL := "https://example.com"
content := "test"
hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
root := fmt.Sprintf("/api/packages/%s/pypi", user.Name)
uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
createBasicMultipartFile := func(filename, packageName, content string) (body *bytes.Buffer, writer *multipart.Writer, closer func() error) {
body = &bytes.Buffer{}
writer = multipart.NewWriter(body)
part, _ := writer.CreateFormFile("content", filename)
_, _ = io.Copy(part, strings.NewReader(content))
@ -53,14 +54,27 @@ func TestPackagePyPI(t *testing.T) {
writer.WriteField("sha256_digest", hashSHA256)
writer.WriteField("requires_python", "3.6")
_ = writer.Close()
return body, writer, writer.Close
}
uploadHelper := func(t *testing.T, body *bytes.Buffer, contentType string, expectedStatus int) {
req := NewRequestWithBody(t, "POST", root, body).
SetHeader("Content-Type", writer.FormDataContentType()).
SetHeader("Content-Type", contentType).
AddBasicAuth(user.Name)
MakeRequest(t, req, expectedStatus)
}
uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
body, writer, closeFunc := createBasicMultipartFile(filename, packageName, content)
writer.WriteField("project_urls", "DOCUMENTATION , https://readthedocs.org")
writer.WriteField("project_urls", fmt.Sprintf("Home-page, %s", projectURL))
_ = closeFunc()
uploadHelper(t, body, writer.FormDataContentType(), expectedStatus)
}
t.Run("Upload", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -75,6 +89,7 @@ func TestPackagePyPI(t *testing.T) {
require.NoError(t, err)
assert.Nil(t, pd.SemVer)
assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL)
assert.Equal(t, packageName, pd.Package.Name)
assert.Equal(t, packageVersion, pd.Version.Version)
@ -134,6 +149,48 @@ func TestPackagePyPI(t *testing.T) {
uploadFile(t, "test.tar.gz", content, http.StatusConflict)
})
t.Run("UploadUsingDeprecatedHomepageMetadata", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
pkgName := "homepage-package"
body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content)
writer.WriteField("home_page", projectURL)
_ = closeFunc()
uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated)
pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName)
require.NoError(t, err)
assert.Len(t, pvs, 1)
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
require.NoError(t, err)
assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL)
})
t.Run("UploadWithoutAnyHomepageURLMetadata", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
pkgName := "no-project-url-or-homepage-package"
body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content)
_ = closeFunc()
uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated)
pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName)
require.NoError(t, err)
assert.Len(t, pvs, 1)
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
require.NoError(t, err)
assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
assert.Empty(t, pd.Metadata.(*pypi.Metadata).ProjectURL)
})
t.Run("Download", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -148,7 +205,7 @@ func TestPackagePyPI(t *testing.T) {
downloadFile("test.whl")
downloadFile("test.tar.gz")
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, packageName)
require.NoError(t, err)
assert.Len(t, pvs, 1)
assert.Equal(t, int64(2), pvs[0].DownloadCount)

View file

@ -159,18 +159,6 @@ func TestMain(m *testing.M) {
}
}
os.Unsetenv("GIT_AUTHOR_NAME")
os.Unsetenv("GIT_AUTHOR_EMAIL")
os.Unsetenv("GIT_AUTHOR_DATE")
os.Unsetenv("GIT_COMMITTER_NAME")
os.Unsetenv("GIT_COMMITTER_EMAIL")
os.Unsetenv("GIT_COMMITTER_DATE")
// Avoid loading the default system config. On MacOS, this config
// sets the osxkeychain credential helper, which will cause tests
// to freeze with a dialog.
os.Setenv("GIT_CONFIG_NOSYSTEM", "true")
err := unittest.InitFixtures(
unittest.FixturesOptions{
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),