mirror of
https://github.com/go-gitea/gitea.git
synced 2025-11-05 18:32:41 +09:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
486d274be6 | ||
|
|
ab3d2a944c | ||
|
|
12bfa9e83d | ||
|
|
dd661e92df | ||
|
|
0b31272c7e | ||
|
|
ec0c418719 | ||
|
|
6dc19fc29a | ||
|
|
9f1baa7d18 | ||
|
|
e13deb7a16 | ||
|
|
e5c1b8b632 | ||
|
|
e931b62f33 | ||
|
|
81ee93e5bc | ||
|
|
053f9186bc | ||
|
|
68fcdb6122 | ||
|
|
14ca309c39 | ||
|
|
4aba42519d | ||
|
|
9adf175df0 | ||
|
|
c3fa2a8729 | ||
|
|
89dfed32e0 | ||
|
|
d5062d0c27 | ||
|
|
90e9e79232 | ||
|
|
c6467edcb1 | ||
|
|
5d5b695527 | ||
|
|
0af7a7b79f | ||
|
|
9339661078 | ||
|
|
1e69f085d6 | ||
|
|
0bfccd8ecf | ||
|
|
534b9b35dd | ||
|
|
dbadc59b56 | ||
|
|
a57e2c4bc3 |
32
CHANGELOG.md
32
CHANGELOG.md
@@ -4,6 +4,38 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.24.3](https://github.com/go-gitea/gitea/releases/tag/1.24.3) - 2025-07-15
|
||||
|
||||
* BUGFIXES
|
||||
* Fix form property assignment edge case (#35073) (#35078)
|
||||
* Improve submodule relative path handling (#35056) (#35075)
|
||||
* Fix incorrect comment diff hunk parsing, fix github asset ID nil panic (#35046) (#35055)
|
||||
* Fix updating user visibility (#35036) (#35044)
|
||||
* Support base64-encoded agit push options (#35037) (#35041)
|
||||
* Make submodule link work with relative path (#35034) (#35038)
|
||||
* Fix bug when displaying git user avatar in commits list (#35006)
|
||||
* Fix API response for swagger spec (#35029)
|
||||
* Start automerge check again after the conflict check and the schedule (#34988) (#35002)
|
||||
* Fix the response format for actions/workflows (#35009) (#35016)
|
||||
* Fix repo settings and protocol log problems (#35012) (#35013)
|
||||
* Fix project images scroll (#34971) (#34972)
|
||||
* Mark old reviews as stale on agit pr updates (#34933) (#34965)
|
||||
* Fix git graph page (#34948) (#34949)
|
||||
* Don't send trigger for a pending review's comment create/update/delete (#34928) (#34939)
|
||||
* Fix some log and UI problems (#34863) (#34868)
|
||||
* Fix archive API (#34853) (#34857)
|
||||
* Ignore force pushes for changed files in a PR review (#34837) (#34843)
|
||||
* Fix SSH LFS timeout (#34838) (#34842)
|
||||
* Fix team permissions (#34827) (#34836)
|
||||
* Fix job status aggregation logic (#34823) (#34835)
|
||||
* Fix issue filter (#34914) (#34915)
|
||||
* Fix typo in pull request merge warning message text (#34899) (#34903)
|
||||
* Support the open-icon of folder (#34168) (#34896)
|
||||
* Optimize flex layout of release attachment area (#34885) (#34886)
|
||||
* Fix the issue of abnormal interface when there is no issue-item on the project page (#34791) (#34880)
|
||||
* Skip updating timestamp when sync branch (#34875)
|
||||
* Fix required contexts and commit status matching bug (#34815) (#34829)
|
||||
|
||||
## [1.24.2](https://github.com/go-gitea/gitea/releases/tag/1.24.2) - 2025-06-20
|
||||
|
||||
* BUGFIXES
|
||||
|
||||
@@ -185,10 +185,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
||||
return StatusSuccess
|
||||
case hasCancelled:
|
||||
return StatusCancelled
|
||||
case hasFailure:
|
||||
return StatusFailure
|
||||
case hasRunning:
|
||||
return StatusRunning
|
||||
case hasFailure:
|
||||
return StatusFailure
|
||||
case hasWaiting:
|
||||
return StatusWaiting
|
||||
case hasBlocked:
|
||||
|
||||
@@ -58,14 +58,14 @@ func TestAggregateJobStatus(t *testing.T) {
|
||||
{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
|
||||
{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
|
||||
|
||||
// failure with other status, fail fast
|
||||
// Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast.
|
||||
// failure with other status, usually fail fast, but "running" wins to match GitHub's behavior
|
||||
// another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail.
|
||||
{[]Status{StatusFailure}, StatusFailure},
|
||||
{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
|
||||
{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
|
||||
{[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
|
||||
{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
|
||||
{[]Status{StatusFailure, StatusRunning}, StatusFailure},
|
||||
{[]Status{StatusFailure, StatusRunning}, StatusRunning},
|
||||
{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
|
||||
|
||||
// skipped with other status
|
||||
|
||||
@@ -91,7 +91,7 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err)
|
||||
return nil, ErrGPGInvalidTokenSignature{
|
||||
ID: ekeys[0].PrimaryKey.KeyIdString(),
|
||||
Wrapped: err,
|
||||
|
||||
@@ -85,7 +85,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
|
||||
}
|
||||
|
||||
if signer == nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
log.Debug("VerifyGPGKey failed: no signer")
|
||||
return "", ErrGPGInvalidTokenSignature{
|
||||
ID: key.KeyID,
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
|
||||
// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
|
||||
// see https://github.com/PowerShell/PowerShell/issues/5974
|
||||
if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
log.Debug("VerifySSHKey sshsig.Verify failed: %v", err)
|
||||
return "", ErrSSHInvalidTokenSignature{
|
||||
Fingerprint: key.Fingerprint,
|
||||
}
|
||||
|
||||
@@ -518,7 +518,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre
|
||||
return currentWhitelist, nil
|
||||
}
|
||||
|
||||
teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
|
||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
|
||||
}
|
||||
|
||||
@@ -719,7 +719,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Comment) loadReview(ctx context.Context) (err error) {
|
||||
// LoadReview loads the associated review
|
||||
func (c *Comment) LoadReview(ctx context.Context) (err error) {
|
||||
if c.ReviewID == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -736,11 +737,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadReview loads the associated review
|
||||
func (c *Comment) LoadReview(ctx context.Context) error {
|
||||
return c.loadReview(ctx)
|
||||
}
|
||||
|
||||
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
|
||||
func (c *Comment) DiffSide() string {
|
||||
if c.Line < 0 {
|
||||
@@ -860,7 +856,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
|
||||
}
|
||||
if comment.ReviewID != 0 {
|
||||
if comment.Review == nil {
|
||||
if err := comment.loadReview(ctx); err != nil {
|
||||
if err := comment.LoadReview(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,8 +602,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
|
||||
"team_user.uid": userID,
|
||||
})
|
||||
}
|
||||
|
||||
// TeamsWithAccessToRepo returns all teams that have given access level to the repository.
|
||||
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
|
||||
return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// TeamRepo represents an team-repository relation.
|
||||
@@ -48,26 +50,27 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository.
|
||||
func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode perm.AccessMode) ([]*Team, error) {
|
||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
return teams, db.GetEngine(ctx).Where("team.authorize >= ?", mode).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
}
|
||||
|
||||
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
|
||||
teams := make([]*Team, 0, 5)
|
||||
return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
|
||||
sub := builder.Select("team_id").From("team_unit").
|
||||
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
||||
|
||||
err := db.GetEngine(ctx).
|
||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||
Join("INNER", "team_unit", "team_unit.team_id = team.id").
|
||||
And("team_repo.org_id = ?", orgID).
|
||||
And("team_repo.repo_id = ?", repoID).
|
||||
And("team_unit.type = ?", unitType).
|
||||
And(builder.Or(
|
||||
builder.Expr("team.authorize >= ?", mode),
|
||||
builder.In("team.id", sub),
|
||||
)).
|
||||
OrderBy("name").
|
||||
Find(&teams)
|
||||
|
||||
return teams, err
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
|
||||
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
|
||||
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
|
||||
|
||||
teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, teams, 2) {
|
||||
assert.EqualValues(t, 21, teams[0].ID)
|
||||
|
||||
@@ -42,6 +42,7 @@ func (p *Permission) IsAdmin() bool {
|
||||
|
||||
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
|
||||
// It doesn't count the "public(anonymous/everyone) access mode".
|
||||
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
|
||||
func (p *Permission) HasAnyUnitAccess() bool {
|
||||
for _, v := range p.unitsMode {
|
||||
if v >= perm_model.AccessModeRead {
|
||||
@@ -267,7 +268,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
perm.units = repo.Units
|
||||
|
||||
// anonymous user visit private repo.
|
||||
// TODO: anonymous user visit public unit of private repo???
|
||||
if user == nil && repo.IsPrivate {
|
||||
perm.AccessMode = perm_model.AccessModeNone
|
||||
return perm, nil
|
||||
@@ -286,7 +286,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
}
|
||||
|
||||
// Prevent strangers from checking out public repo of private organization/users
|
||||
// Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself
|
||||
// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
|
||||
// TODO: rename it to "IsOwnerVisibleToDoer"
|
||||
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
|
||||
perm.AccessMode = perm_model.AccessModeNone
|
||||
return perm, nil
|
||||
@@ -304,7 +305,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// plain user
|
||||
// plain user TODO: this check should be replaced, only need to check collaborator access mode
|
||||
perm.AccessMode, err = accessLevel(ctx, user, repo)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
@@ -314,6 +315,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// now: the owner is visible to doer, if the repo is public, then the min access mode is read
|
||||
minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone)
|
||||
perm.AccessMode = max(perm.AccessMode, minAccessMode)
|
||||
|
||||
// get units mode from teams
|
||||
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
if len(teams) == 0 {
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
|
||||
|
||||
// Collaborators on organization
|
||||
@@ -323,12 +337,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
}
|
||||
}
|
||||
|
||||
// get units mode from teams
|
||||
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
// if user in an owner team
|
||||
for _, team := range teams {
|
||||
if team.HasAdminAccess() {
|
||||
@@ -339,19 +347,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
}
|
||||
|
||||
for _, u := range repo.Units {
|
||||
var found bool
|
||||
for _, team := range teams {
|
||||
unitAccessMode := minAccessMode
|
||||
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
|
||||
perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
|
||||
if !found && !repo.IsPrivate && !user.IsRestricted {
|
||||
if _, ok := perm.unitsMode[u.Type]; !ok {
|
||||
perm.unitsMode[u.Type] = perm_model.AccessModeRead
|
||||
unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode)
|
||||
}
|
||||
perm.unitsMode[u.Type] = unitAccessMode
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,16 @@ package access
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHasAnyUnitAccess(t *testing.T) {
|
||||
@@ -152,3 +156,45 @@ func TestUnitAccessMode(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
|
||||
}
|
||||
|
||||
func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ctx := t.Context()
|
||||
repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
|
||||
require.NoError(t, repo32.LoadOwner(ctx))
|
||||
require.True(t, repo32.Owner.IsOrganization())
|
||||
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}))
|
||||
org := repo32.Owner
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
||||
require.NoError(t, db.Insert(ctx, team))
|
||||
|
||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
|
||||
})
|
||||
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||
t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||
})
|
||||
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
|
||||
t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ package pull
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// AutoMerge represents a pull request scheduled for merging when checks succeed
|
||||
@@ -76,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
|
||||
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
doer, err = user_model.NewGhostUser(), nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
@@ -1176,12 +1176,14 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
|
||||
|
||||
needCheckEmails := make(container.Set[string])
|
||||
needCheckUserNames := make(container.Set[string])
|
||||
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
|
||||
for _, email := range emails {
|
||||
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
|
||||
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
|
||||
needCheckUserNames.Add(strings.ToLower(username))
|
||||
emailLower := strings.ToLower(email)
|
||||
if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
|
||||
needCheckUserNames.Add(noReplyUserNameLower)
|
||||
needCheckEmails.Add(emailLower)
|
||||
} else {
|
||||
needCheckEmails.Add(strings.ToLower(email))
|
||||
needCheckEmails.Add(emailLower)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ func TestUserEmails(t *testing.T) {
|
||||
testGetUserByEmail(t, c.Email, c.UID)
|
||||
})
|
||||
}
|
||||
t.Run("NoReplyConflict", func(t *testing.T) {
|
||||
setting.Service.NoReplyAddress = "example.com"
|
||||
testGetUserByEmail(t, "user1-2@example.COM", 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,22 +6,26 @@ package fileicon
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func BasicThemeIcon(entry *git.TreeEntry) template.HTML {
|
||||
func BasicEntryIconName(entry *EntryInfo) string {
|
||||
svgName := "octicon-file"
|
||||
switch {
|
||||
case entry.IsLink():
|
||||
case entry.EntryMode.IsLink():
|
||||
svgName = "octicon-file-symlink-file"
|
||||
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
|
||||
if entry.SymlinkToMode.IsDir() {
|
||||
svgName = "octicon-file-directory-symlink"
|
||||
}
|
||||
case entry.IsDir():
|
||||
svgName = "octicon-file-directory-fill"
|
||||
case entry.IsSubModule():
|
||||
case entry.EntryMode.IsDir():
|
||||
svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill")
|
||||
case entry.EntryMode.IsSubModule():
|
||||
svgName = "octicon-file-submodule"
|
||||
}
|
||||
return svg.RenderHTML(svgName)
|
||||
return svgName
|
||||
}
|
||||
|
||||
func BasicEntryIconHTML(entry *EntryInfo) template.HTML {
|
||||
return svg.RenderHTML(BasicEntryIconName(entry))
|
||||
}
|
||||
|
||||
31
modules/fileicon/entry.go
Normal file
31
modules/fileicon/entry.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package fileicon
|
||||
|
||||
import "code.gitea.io/gitea/modules/git"
|
||||
|
||||
type EntryInfo struct {
|
||||
FullName string
|
||||
EntryMode git.EntryMode
|
||||
SymlinkToMode git.EntryMode
|
||||
IsOpen bool
|
||||
}
|
||||
|
||||
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo {
|
||||
ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
|
||||
if gitEntry.IsLink() {
|
||||
if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() {
|
||||
ret.SymlinkToMode = te.Mode()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func EntryInfoFolder() *EntryInfo {
|
||||
return &EntryInfo{EntryMode: git.EntryModeTree}
|
||||
}
|
||||
|
||||
func EntryInfoFolderOpen() *EntryInfo {
|
||||
return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true}
|
||||
}
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type materialIconRulesData struct {
|
||||
@@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
|
||||
}
|
||||
svgID := "svg-mfi-" + name
|
||||
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
|
||||
svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
||||
if p == nil {
|
||||
return svgHTML
|
||||
}
|
||||
if p.IconSVGs[svgID] == "" {
|
||||
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
||||
p.IconSVGs[svgID] = svgHTML
|
||||
}
|
||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
||||
}
|
||||
|
||||
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
|
||||
if m.rules == nil {
|
||||
return BasicThemeIcon(entry)
|
||||
return BasicEntryIconHTML(entry)
|
||||
}
|
||||
|
||||
if entry.IsLink() {
|
||||
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
|
||||
if entry.EntryMode.IsLink() {
|
||||
if entry.SymlinkToMode.IsDir() {
|
||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
||||
return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
|
||||
}
|
||||
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
|
||||
}
|
||||
|
||||
name := m.findIconNameByGit(entry)
|
||||
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
|
||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
||||
if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" {
|
||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
||||
extraClass := "octicon-file"
|
||||
switch {
|
||||
case entry.IsDir():
|
||||
extraClass = "octicon-file-directory-fill"
|
||||
case entry.IsSubModule():
|
||||
extraClass = "octicon-file-submodule"
|
||||
name := m.FindIconName(entry)
|
||||
iconSVG := m.svgs[name]
|
||||
if iconSVG == "" {
|
||||
name = "file"
|
||||
if entry.EntryMode.IsDir() {
|
||||
name = util.Iif(entry.IsOpen, "folder-open", "folder")
|
||||
}
|
||||
iconSVG = m.svgs[name]
|
||||
if iconSVG == "" {
|
||||
setting.PanicInDevOrTesting("missing file icon for %s", name)
|
||||
}
|
||||
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
|
||||
}
|
||||
// TODO: use an interface or wrapper for git.Entry to make the code testable.
|
||||
return BasicThemeIcon(entry)
|
||||
|
||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
||||
extraClass := "octicon-file"
|
||||
switch {
|
||||
case entry.EntryMode.IsDir():
|
||||
extraClass = BasicEntryIconName(entry)
|
||||
case entry.EntryMode.IsSubModule():
|
||||
extraClass = "octicon-file-submodule"
|
||||
}
|
||||
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
|
||||
}
|
||||
|
||||
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
|
||||
@@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
|
||||
fileNameLower := strings.ToLower(path.Base(name))
|
||||
if isDir {
|
||||
func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
|
||||
if entry.EntryMode.IsSubModule() {
|
||||
return "folder-git"
|
||||
}
|
||||
|
||||
fileNameLower := strings.ToLower(path.Base(entry.FullName))
|
||||
if entry.EntryMode.IsDir() {
|
||||
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
|
||||
return s
|
||||
}
|
||||
return "folder"
|
||||
return util.Iif(entry.IsOpen, "folder-open", "folder")
|
||||
}
|
||||
|
||||
if s, ok := m.rules.FileNames[fileNameLower]; ok {
|
||||
@@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
|
||||
|
||||
return "file"
|
||||
}
|
||||
|
||||
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
|
||||
if entry.IsSubModule() {
|
||||
return "folder-git"
|
||||
}
|
||||
return m.FindIconName(entry.Name(), entry.IsDir())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -19,8 +20,8 @@ func TestMain(m *testing.M) {
|
||||
func TestFindIconName(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
p := fileicon.DefaultMaterialIconProvider()
|
||||
assert.Equal(t, "php", p.FindIconName("foo.php", false))
|
||||
assert.Equal(t, "php", p.FindIconName("foo.PHP", false))
|
||||
assert.Equal(t, "javascript", p.FindIconName("foo.js", false))
|
||||
assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false))
|
||||
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
|
||||
assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
@@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
|
||||
return template.HTML(sb.String())
|
||||
}
|
||||
|
||||
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
|
||||
|
||||
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||
func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
|
||||
if setting.UI.FileIconTheme == "material" {
|
||||
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
||||
return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
|
||||
}
|
||||
return BasicThemeIcon(entry)
|
||||
}
|
||||
|
||||
func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
||||
// TODO: add "open icon" support
|
||||
if setting.UI.FileIconTheme == "material" {
|
||||
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
||||
}
|
||||
return BasicThemeIcon(entry)
|
||||
return BasicEntryIconHTML(entry)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,20 @@
|
||||
|
||||
package git
|
||||
|
||||
import "path"
|
||||
|
||||
// CommitInfo describes the first commit with the provided entry
|
||||
type CommitInfo struct {
|
||||
Entry *TreeEntry
|
||||
Commit *Commit
|
||||
SubmoduleFile *CommitSubmoduleFile
|
||||
}
|
||||
|
||||
func getCommitInfoSubmoduleFile(repoLink string, entry *TreeEntry, commit *Commit, treePathDir string) (*CommitSubmoduleFile, error) {
|
||||
fullPath := path.Join(treePathDir, entry.Name())
|
||||
submodule, err := commit.GetSubModule(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, entry.ID.String()), nil
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||
entryPaths := make([]string, len(tes)+1)
|
||||
// Get the commit for the treePath itself
|
||||
entryPaths[0] = ""
|
||||
@@ -71,22 +71,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
||||
commitsInfo[i].Commit = entryCommit
|
||||
}
|
||||
|
||||
// If the entry is a submodule add a submodule file for this
|
||||
// If the entry is a submodule, add a submodule file for this
|
||||
if entry.IsSubModule() {
|
||||
subModuleURL := ""
|
||||
var fullPath string
|
||||
if len(treePath) > 0 {
|
||||
fullPath = treePath + "/" + entry.Name()
|
||||
} else {
|
||||
fullPath = entry.Name()
|
||||
}
|
||||
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
||||
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if subModule != nil {
|
||||
subModuleURL = subModule.URL
|
||||
}
|
||||
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
|
||||
commitsInfo[i].SubmoduleFile = subModuleFile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||
entryPaths := make([]string, len(tes)+1)
|
||||
// Get the commit for the treePath itself
|
||||
entryPaths[0] = ""
|
||||
@@ -65,22 +65,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
||||
log.Debug("missing commit for %s", entry.Name())
|
||||
}
|
||||
|
||||
// If the entry is a submodule add a submodule file for this
|
||||
// If the entry is a submodule, add a submodule file for this
|
||||
if entry.IsSubModule() {
|
||||
subModuleURL := ""
|
||||
var fullPath string
|
||||
if len(treePath) > 0 {
|
||||
fullPath = treePath + "/" + entry.Name()
|
||||
} else {
|
||||
fullPath = entry.Name()
|
||||
}
|
||||
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
||||
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if subModule != nil {
|
||||
subModuleURL = subModule.URL
|
||||
}
|
||||
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
|
||||
commitsInfo[i].SubmoduleFile = subModuleFile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
||||
}
|
||||
|
||||
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
|
||||
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), commit, testCase.Path)
|
||||
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path)
|
||||
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
||||
if err != nil {
|
||||
t.FailNow()
|
||||
@@ -159,7 +159,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
b.Run(benchmark.name, func(b *testing.B) {
|
||||
for b.Loop() {
|
||||
_, _, err := entries.GetCommitsInfo(b.Context(), commit, "")
|
||||
_, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,49 +6,61 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// CommitSubmoduleFile represents a file with submodule type.
|
||||
type CommitSubmoduleFile struct {
|
||||
refURL string
|
||||
parsedURL *giturl.RepositoryURL
|
||||
parsed bool
|
||||
refID string
|
||||
repoLink string
|
||||
repoLink string
|
||||
fullPath string
|
||||
refURL string
|
||||
refID string
|
||||
|
||||
parsed bool
|
||||
parsedTargetLink string
|
||||
}
|
||||
|
||||
// NewCommitSubmoduleFile create a new submodule file
|
||||
func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile {
|
||||
return &CommitSubmoduleFile{refURL: refURL, refID: refID}
|
||||
func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSubmoduleFile {
|
||||
return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID}
|
||||
}
|
||||
|
||||
func (sf *CommitSubmoduleFile) RefID() string {
|
||||
return sf.refID // this function is only used in templates
|
||||
return sf.refID
|
||||
}
|
||||
|
||||
// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver
|
||||
func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
|
||||
func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink {
|
||||
if sf == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(sf.refURL, "../") {
|
||||
targetLink := path.Join(sf.repoLink, path.Dir(sf.fullPath), sf.refURL)
|
||||
return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
|
||||
}
|
||||
if !sf.parsed {
|
||||
sf.parsed = true
|
||||
parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
sf.parsedURL = parsedURL
|
||||
sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL)
|
||||
sf.parsedTargetLink = giturl.MakeRepositoryWebLink(parsedURL)
|
||||
}
|
||||
var commitLink string
|
||||
if len(optCommitID) == 2 {
|
||||
commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1]
|
||||
} else if len(optCommitID) == 1 {
|
||||
commitLink = sf.repoLink + "/tree/" + optCommitID[0]
|
||||
} else {
|
||||
commitLink = sf.repoLink + "/tree/" + sf.refID
|
||||
}
|
||||
return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink}
|
||||
return &SubmoduleWebLink{RepoWebLink: sf.parsedTargetLink, CommitWebLink: sf.parsedTargetLink + moreLinkPath}
|
||||
}
|
||||
|
||||
// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver
|
||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
|
||||
if sf == nil {
|
||||
return nil
|
||||
}
|
||||
return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.refID))
|
||||
}
|
||||
|
||||
// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver
|
||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink {
|
||||
return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2)
|
||||
}
|
||||
|
||||
@@ -10,20 +10,29 @@ import (
|
||||
)
|
||||
|
||||
func TestCommitSubmoduleLink(t *testing.T) {
|
||||
sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa")
|
||||
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context()))
|
||||
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", ""))
|
||||
|
||||
wl := sf.SubmoduleWebLink(t.Context())
|
||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
|
||||
t.Run("GitHubRepo", func(t *testing.T) {
|
||||
sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa")
|
||||
wl := sf.SubmoduleWebLinkTree(t.Context())
|
||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
|
||||
|
||||
wl = sf.SubmoduleWebLink(t.Context(), "1111")
|
||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink)
|
||||
wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
|
||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
|
||||
})
|
||||
|
||||
wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222")
|
||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
|
||||
t.Run("RelativePath", func(t *testing.T) {
|
||||
sf := NewCommitSubmoduleFile("/subpath/any/repo-home-link", "full-path", "../../user/repo", "aaaa")
|
||||
wl := sf.SubmoduleWebLinkTree(t.Context())
|
||||
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink)
|
||||
|
||||
wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context())
|
||||
assert.Nil(t, wl)
|
||||
sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../../user/repo", "aaaa")
|
||||
wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
|
||||
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,9 +99,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseDiffHunkString parse the diffhunk content and return
|
||||
func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) {
|
||||
ss := strings.Split(diffhunk, "@@")
|
||||
// ParseDiffHunkString parse the diff hunk content and return
|
||||
func ParseDiffHunkString(diffHunk string) (leftLine, leftHunk, rightLine, rightHunk int) {
|
||||
ss := strings.Split(diffHunk, "@@")
|
||||
ranges := strings.Split(ss[1][1:], " ")
|
||||
leftRange := strings.Split(ranges[0], ",")
|
||||
leftLine, _ = strconv.Atoi(leftRange[0][1:])
|
||||
@@ -112,14 +112,19 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu
|
||||
rightRange := strings.Split(ranges[1], ",")
|
||||
rightLine, _ = strconv.Atoi(rightRange[0])
|
||||
if len(rightRange) > 1 {
|
||||
righHunk, _ = strconv.Atoi(rightRange[1])
|
||||
rightHunk, _ = strconv.Atoi(rightRange[1])
|
||||
}
|
||||
} else {
|
||||
log.Debug("Parse line number failed: %v", diffhunk)
|
||||
log.Debug("Parse line number failed: %v", diffHunk)
|
||||
rightLine = leftLine
|
||||
righHunk = leftHunk
|
||||
rightHunk = leftHunk
|
||||
}
|
||||
return leftLine, leftHunk, rightLine, righHunk
|
||||
if rightLine == 0 {
|
||||
// FIXME: GIT-DIFF-CUT-BUG search this tag to see details
|
||||
// this is only a hacky patch, the rightLine&rightHunk might still be incorrect in some cases.
|
||||
rightLine++
|
||||
}
|
||||
return leftLine, leftHunk, rightLine, rightHunk
|
||||
}
|
||||
|
||||
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
|
||||
@@ -270,6 +275,12 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
||||
oldNumOfLines++
|
||||
}
|
||||
}
|
||||
|
||||
// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
|
||||
// FIXME: GIT-DIFF-CUT-BUG But there is a bug in CutDiffAroundLine, then the "Patch" stored in the comment model becomes "@@ -1,1 +0,4 @@"
|
||||
// It may generate incorrect results for difference cases, for example: delete 2 line add 1 line, delete 2 line add 2 line etc, need to double check.
|
||||
// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
|
||||
|
||||
// construct the new hunk header
|
||||
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
||||
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
||||
|
||||
@@ -30,6 +30,31 @@ func (e EntryMode) String() string {
|
||||
return strconv.FormatInt(int64(e), 8)
|
||||
}
|
||||
|
||||
// IsSubModule if the entry is a sub module
|
||||
func (e EntryMode) IsSubModule() bool {
|
||||
return e == EntryModeCommit
|
||||
}
|
||||
|
||||
// IsDir if the entry is a sub dir
|
||||
func (e EntryMode) IsDir() bool {
|
||||
return e == EntryModeTree
|
||||
}
|
||||
|
||||
// IsLink if the entry is a symlink
|
||||
func (e EntryMode) IsLink() bool {
|
||||
return e == EntryModeSymlink
|
||||
}
|
||||
|
||||
// IsRegular if the entry is a regular file
|
||||
func (e EntryMode) IsRegular() bool {
|
||||
return e == EntryModeBlob
|
||||
}
|
||||
|
||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||
func (e EntryMode) IsExecutable() bool {
|
||||
return e == EntryModeExec
|
||||
}
|
||||
|
||||
func ParseEntryMode(mode string) (EntryMode, error) {
|
||||
switch mode {
|
||||
case "000000":
|
||||
|
||||
@@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
|
||||
|
||||
// IsSubModule if the entry is a sub module
|
||||
func (te *TreeEntry) IsSubModule() bool {
|
||||
return te.entryMode == EntryModeCommit
|
||||
return te.entryMode.IsSubModule()
|
||||
}
|
||||
|
||||
// IsDir if the entry is a sub dir
|
||||
func (te *TreeEntry) IsDir() bool {
|
||||
return te.entryMode == EntryModeTree
|
||||
return te.entryMode.IsDir()
|
||||
}
|
||||
|
||||
// IsLink if the entry is a symlink
|
||||
func (te *TreeEntry) IsLink() bool {
|
||||
return te.entryMode == EntryModeSymlink
|
||||
return te.entryMode.IsLink()
|
||||
}
|
||||
|
||||
// IsRegular if the entry is a regular file
|
||||
func (te *TreeEntry) IsRegular() bool {
|
||||
return te.entryMode == EntryModeBlob
|
||||
return te.entryMode.IsRegular()
|
||||
}
|
||||
|
||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||
func (te *TreeEntry) IsExecutable() bool {
|
||||
return te.entryMode == EntryModeExec
|
||||
return te.entryMode.IsExecutable()
|
||||
}
|
||||
|
||||
// Blob returns the blob object the entry
|
||||
|
||||
@@ -132,6 +132,7 @@ func newInternalRequestLFS(ctx context.Context, internalURL, method string, head
|
||||
return nil
|
||||
}
|
||||
req := private.NewInternalRequest(ctx, internalURL, method)
|
||||
req.SetReadWriteTimeout(0)
|
||||
for k, v := range headers {
|
||||
req.Header(k, v)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,13 @@ func FromPtr[T any](v *T) Option[T] {
|
||||
return Some(*v)
|
||||
}
|
||||
|
||||
func FromMapLookup[K comparable, V any](m map[K]V, k K) Option[V] {
|
||||
if v, ok := m[k]; ok {
|
||||
return Some(v)
|
||||
}
|
||||
return None[V]()
|
||||
}
|
||||
|
||||
func FromNonDefault[T comparable](v T) Option[T] {
|
||||
var zero T
|
||||
if v == zero {
|
||||
|
||||
@@ -56,6 +56,12 @@ func TestOption(t *testing.T) {
|
||||
opt3 := optional.FromNonDefault(1)
|
||||
assert.True(t, opt3.Has())
|
||||
assert.Equal(t, int(1), opt3.Value())
|
||||
|
||||
opt4 := optional.FromMapLookup(map[string]int{"a": 1}, "a")
|
||||
assert.True(t, opt4.Has())
|
||||
assert.Equal(t, 1, opt4.Value())
|
||||
opt4 = optional.FromMapLookup(map[string]int{"a": 1}, "b")
|
||||
assert.False(t, opt4.Has())
|
||||
}
|
||||
|
||||
func Test_ParseBool(t *testing.T) {
|
||||
|
||||
@@ -41,11 +41,14 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("GetObjectFormat: %w", err)
|
||||
}
|
||||
_, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("UpdateRepository: %w", err)
|
||||
|
||||
if repo.ObjectFormatName != objFmt.Name() {
|
||||
repo.ObjectFormatName = objFmt.Name()
|
||||
_, err = db.GetEngine(ctx).ID(repo.ID).NoAutoTime().Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("UpdateRepository: %w", err)
|
||||
}
|
||||
}
|
||||
repo.ObjectFormatName = objFmt.Name() // keep consistent with db
|
||||
|
||||
allBranches := container.Set[string]{}
|
||||
{
|
||||
|
||||
@@ -275,7 +275,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
|
||||
}
|
||||
default:
|
||||
log.Fatal("Invalid PROTOCOL %q", Protocol)
|
||||
log.Fatal("Invalid PROTOCOL %q", protocolCfg)
|
||||
}
|
||||
UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
|
||||
ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false)
|
||||
|
||||
@@ -57,7 +57,7 @@ type Repository struct {
|
||||
Private bool `json:"private"`
|
||||
Fork bool `json:"fork"`
|
||||
Template bool `json:"template"`
|
||||
Parent *Repository `json:"parent"`
|
||||
Parent *Repository `json:"parent,omitempty"`
|
||||
Mirror bool `json:"mirror"`
|
||||
Size int `json:"size"`
|
||||
Language string `json:"language"`
|
||||
@@ -112,7 +112,7 @@ type Repository struct {
|
||||
ObjectFormatName string `json:"object_format_name"`
|
||||
// swagger:strfmt date-time
|
||||
MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer"`
|
||||
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
|
||||
Topics []string `json:"topics"`
|
||||
Licenses []string `json:"licenses"`
|
||||
}
|
||||
|
||||
@@ -1957,7 +1957,7 @@ pulls.cmd_instruction_checkout_title = Checkout
|
||||
pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes.
|
||||
pulls.cmd_instruction_merge_title = Merge
|
||||
pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea.
|
||||
pulls.cmd_instruction_merge_warning = Warning: This operation can not merge pull request because "autodetect manual merge" was not enable
|
||||
pulls.cmd_instruction_merge_warning = Warning: This operation cannot merge pull request because "autodetect manual merge" is not enabled.
|
||||
pulls.clear_merge_message = Clear merge message
|
||||
pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …".
|
||||
|
||||
@@ -2153,6 +2153,7 @@ settings.collaboration.write = Write
|
||||
settings.collaboration.read = Read
|
||||
settings.collaboration.owner = Owner
|
||||
settings.collaboration.undefined = Undefined
|
||||
settings.collaboration.per_unit = Unit Permissions
|
||||
settings.hooks = Webhooks
|
||||
settings.githooks = Git Hooks
|
||||
settings.basic_settings = Basic Settings
|
||||
|
||||
@@ -240,7 +240,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
Description: optional.FromPtr(form.Description),
|
||||
IsActive: optional.FromPtr(form.Active),
|
||||
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
|
||||
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
|
||||
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
|
||||
AllowGitHook: optional.FromPtr(form.AllowGitHook),
|
||||
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
|
||||
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
|
||||
|
||||
@@ -228,7 +228,7 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.HasAnyUnitAccess() {
|
||||
if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
@@ -1241,7 +1241,7 @@ func Routes() *web.Router {
|
||||
}, reqToken())
|
||||
m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
|
||||
m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
|
||||
m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
|
||||
m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
|
||||
m.Combo("/forks").Get(repo.ListForks).
|
||||
Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
|
||||
m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream)
|
||||
@@ -1445,7 +1445,7 @@ func Routes() *web.Router {
|
||||
m.Delete("", repo.DeleteAvatar)
|
||||
}, reqAdmin(), reqToken())
|
||||
|
||||
m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
|
||||
m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
|
||||
}, repoAssignment(), checkTokenPublicOnly())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ func Edit(ctx *context.APIContext) {
|
||||
Description: optional.Some(form.Description),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
|
||||
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
|
||||
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
||||
}
|
||||
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
|
||||
|
||||
@@ -44,5 +44,5 @@ type swaggerResponseActionWorkflow struct {
|
||||
// swagger:response ActionWorkflowList
|
||||
type swaggerResponseActionWorkflowList struct {
|
||||
// in:body
|
||||
Body []api.ActionWorkflow `json:"body"`
|
||||
Body api.ActionWorkflowResponse `json:"body"`
|
||||
}
|
||||
|
||||
@@ -283,11 +283,22 @@ func NewTeam(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future,
|
||||
// admin team won't inherit the correct admin permission for the new unit.
|
||||
// The existing teams won't inherit the correct admin permission for the new unit.
|
||||
// The full history is like this:
|
||||
// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission.
|
||||
// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs.
|
||||
// - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner
|
||||
// - Sometimes, "team unit" is used not really used and "team unit" is used.
|
||||
// - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both.
|
||||
//
|
||||
// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions.
|
||||
// - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units.
|
||||
//
|
||||
// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones.
|
||||
func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode {
|
||||
unitPerms := make(map[unit_model.Type]perm.AccessMode)
|
||||
for _, ut := range unit_model.AllRepoUnitTypes {
|
||||
// Default accessmode is none
|
||||
// Default access mode is none
|
||||
unitPerms[ut] = perm.AccessModeNone
|
||||
|
||||
v, ok := forms[fmt.Sprintf("unit_%d", ut)]
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -168,10 +169,13 @@ func Graph(ctx *context.Context) {
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
|
||||
divOnly := ctx.FormBool("div-only")
|
||||
queryParams := ctx.Req.URL.Query()
|
||||
queryParams.Del("div-only")
|
||||
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
|
||||
paginator.AddParamFromRequest(ctx.Req)
|
||||
paginator.AddParamFromQuery(queryParams)
|
||||
ctx.Data["Page"] = paginator
|
||||
if ctx.FormBool("div-only") {
|
||||
if divOnly {
|
||||
ctx.HTML(http.StatusOK, tplGraphDiv)
|
||||
return
|
||||
}
|
||||
@@ -313,7 +317,7 @@ func Diff(ctx *context.Context) {
|
||||
maxLines, maxFiles = -1, -1
|
||||
}
|
||||
|
||||
diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{
|
||||
diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, &gitdiff.DiffOptions{
|
||||
AfterCommitID: commitID,
|
||||
SkipTo: ctx.FormString("skip-to"),
|
||||
MaxLines: maxLines,
|
||||
@@ -369,7 +373,11 @@ func Diff(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil)
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
|
||||
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
|
||||
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
|
||||
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
||||
}
|
||||
|
||||
statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
csv_module "code.gitea.io/gitea/modules/csv"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -613,7 +614,7 @@ func PrepareCompareDiff(
|
||||
|
||||
fileOnly := ctx.FormBool("file-only")
|
||||
|
||||
diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo,
|
||||
diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadRepo.Link(), ci.HeadGitRepo,
|
||||
&gitdiff.DiffOptions{
|
||||
BeforeCommitID: beforeCommitID,
|
||||
AfterCommitID: headCommitID,
|
||||
@@ -644,7 +645,11 @@ func PrepareCompareDiff(
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil)
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
|
||||
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
|
||||
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
|
||||
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
||||
}
|
||||
|
||||
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
@@ -749,7 +750,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
|
||||
diffOptions.BeforeCommitID = startCommitID
|
||||
}
|
||||
|
||||
diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...)
|
||||
diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, diffOptions, files...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiff", err)
|
||||
return
|
||||
@@ -824,7 +825,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
|
||||
if reviewState != nil {
|
||||
filesViewedState = reviewState.UpdatedFiles
|
||||
}
|
||||
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState)
|
||||
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState)
|
||||
ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
|
||||
ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
|
||||
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
||||
}
|
||||
|
||||
ctx.Data["Diff"] = diff
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@@ -89,7 +90,7 @@ func SettingsProtectedBranch(c *context.Context) {
|
||||
c.Data["recent_status_checks"] = contexts
|
||||
|
||||
if c.Repo.Owner.IsOrganization() {
|
||||
teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c, c.Repo.Repository.ID, perm.AccessModeRead)
|
||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(c, c.Repo.Owner.ID, c.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
@@ -156,7 +157,7 @@ func setTagsContext(ctx *context.Context) error {
|
||||
ctx.Data["Users"] = users
|
||||
|
||||
if ctx.Repo.Owner.IsOrganization() {
|
||||
teams, err := organization.OrgFromUser(ctx.Repo.Owner).TeamsWithAccessToRepo(ctx, ctx.Repo.Repository.ID, perm.AccessModeRead)
|
||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, ctx.Repo.Owner.ID, ctx.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -67,7 +68,7 @@ type WebDiffFileItem struct {
|
||||
EntryMode string
|
||||
IsViewed bool
|
||||
Children []*WebDiffFileItem
|
||||
// TODO: add icon support in the future
|
||||
FileIcon template.HTML
|
||||
}
|
||||
|
||||
// WebDiffFileTree is used by frontend, check the field names in frontend before changing
|
||||
@@ -77,7 +78,7 @@ type WebDiffFileTree struct {
|
||||
|
||||
// transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering
|
||||
// it also takes a map of file names to their viewed state, which is used to mark files as viewed
|
||||
func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
|
||||
func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
|
||||
dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
|
||||
addItem := func(item *WebDiffFileItem) {
|
||||
var parentPath string
|
||||
@@ -110,6 +111,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
|
||||
item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
|
||||
item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
|
||||
item.NameHash = git.HashFilePathForWebUI(item.FullName)
|
||||
item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode})
|
||||
|
||||
switch file.HeadMode {
|
||||
case git.EntryModeTree:
|
||||
@@ -141,7 +143,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
|
||||
|
||||
func TreeViewNodes(ctx *context.Context) {
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
|
||||
results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTreeViewNodes", err)
|
||||
return
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
pull_model "code.gitea.io/gitea/models/pull"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
|
||||
@@ -14,7 +16,8 @@ import (
|
||||
)
|
||||
|
||||
func TestTransformDiffTreeForWeb(t *testing.T) {
|
||||
ret := transformDiffTreeForWeb(&gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
|
||||
{
|
||||
Status: "changed",
|
||||
HeadPath: "dir-a/dir-a-x/file-deep",
|
||||
@@ -29,6 +32,9 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
|
||||
"dir-a/dir-a-x/file-deep": pull_model.Viewed,
|
||||
})
|
||||
|
||||
mockIconForFile := func(id string) template.HTML {
|
||||
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
||||
}
|
||||
assert.Equal(t, WebDiffFileTree{
|
||||
TreeRoot: WebDiffFileItem{
|
||||
Children: []*WebDiffFileItem{
|
||||
@@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
|
||||
NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
|
||||
DiffStatus: "changed",
|
||||
IsViewed: true,
|
||||
FileIcon: mockIconForFile(`svg-mfi-file`),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
|
||||
FullName: "file1",
|
||||
NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
|
||||
DiffStatus: "added",
|
||||
FileIcon: mockIconForFile(`svg-mfi-file`),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -257,8 +257,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
fileIcons := map[string]template.HTML{}
|
||||
for _, f := range files {
|
||||
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry)
|
||||
fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry))
|
||||
}
|
||||
fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
|
||||
ctx.Data["FileIcons"] = fileIcons
|
||||
ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
|
||||
}
|
||||
@@ -298,7 +299,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
|
||||
files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommitsInfo", err)
|
||||
return nil
|
||||
|
||||
@@ -6,7 +6,6 @@ package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
gocontext "context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -666,7 +665,7 @@ func WikiPages(ctx *context.Context) {
|
||||
}
|
||||
allEntries.CustomSort(base.NaturalSortLess)
|
||||
|
||||
entries, _, err := allEntries.GetCommitsInfo(gocontext.Context(ctx), commit, treePath)
|
||||
entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommitsInfo", err)
|
||||
return
|
||||
|
||||
@@ -5,10 +5,12 @@ package agit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -17,17 +19,30 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
)
|
||||
|
||||
func parseAgitPushOptionValue(s string) string {
|
||||
if base64Value, ok := strings.CutPrefix(s, "{base64}"); ok {
|
||||
decoded, err := base64.StdEncoding.DecodeString(base64Value)
|
||||
return util.Iif(err == nil, string(decoded), s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ProcReceive handle proc receive work
|
||||
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
|
||||
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
|
||||
forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
|
||||
topicBranch := opts.GitPushOptions["topic"]
|
||||
title := strings.TrimSpace(opts.GitPushOptions["title"])
|
||||
description := strings.TrimSpace(opts.GitPushOptions["description"])
|
||||
|
||||
// some options are base64-encoded with "{base64}" prefix if they contain new lines
|
||||
// other agit push options like "issue", "reviewer" and "cc" are not supported
|
||||
title := parseAgitPushOptionValue(opts.GitPushOptions["title"])
|
||||
description := parseAgitPushOptionValue(opts.GitPushOptions["description"])
|
||||
|
||||
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
|
||||
userName := strings.ToLower(opts.UserName)
|
||||
|
||||
@@ -199,11 +214,37 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
|
||||
}
|
||||
}
|
||||
|
||||
// Store old commit ID for review staleness checking
|
||||
oldHeadCommitID := pr.HeadCommitID
|
||||
|
||||
pr.HeadCommitID = opts.NewCommitIDs[i]
|
||||
if err = pull_service.UpdateRef(ctx, pr); err != nil {
|
||||
return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
|
||||
}
|
||||
|
||||
// Mark existing reviews as stale when PR content changes (same as regular GitHub flow)
|
||||
if oldHeadCommitID != opts.NewCommitIDs[i] {
|
||||
if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
|
||||
log.Error("MarkReviewsAsStale: %v", err)
|
||||
}
|
||||
|
||||
// Dismiss all approval reviews if protected branch rule item enabled
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
log.Error("GetFirstMatchProtectedBranchRule: %v", err)
|
||||
}
|
||||
if pb != nil && pb.DismissStaleApprovals {
|
||||
if err := pull_service.DismissApprovalReviews(ctx, pusher, pr); err != nil {
|
||||
log.Error("DismissApprovalReviews: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark reviews for the new commit as not stale
|
||||
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitIDs[i]); err != nil {
|
||||
log.Error("MarkReviewsAsNotStale: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
pull_service.StartPullRequestCheckImmediately(ctx, pr)
|
||||
err = pr.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
|
||||
16
services/agit/agit_test.go
Normal file
16
services/agit/agit_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package agit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseAgitPushOptionValue(t *testing.T) {
|
||||
assert.Equal(t, "a", parseAgitPushOptionValue("a"))
|
||||
assert.Equal(t, "a", parseAgitPushOptionValue("{base64}YQ=="))
|
||||
assert.Equal(t, "{base64}invalid value", parseAgitPushOptionValue("{base64}invalid value"))
|
||||
}
|
||||
@@ -22,23 +22,21 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
// prAutoMergeQueue represents a queue to handle update pull request tests
|
||||
var prAutoMergeQueue *queue.WorkerPoolQueue[string]
|
||||
|
||||
// Init runs the task queue to that handles auto merges
|
||||
func Init() error {
|
||||
notify_service.RegisterNotifier(NewNotifier())
|
||||
|
||||
prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
|
||||
if prAutoMergeQueue == nil {
|
||||
automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
|
||||
if automergequeue.AutoMergeQueue == nil {
|
||||
return errors.New("unable to create pr_auto_merge queue")
|
||||
}
|
||||
go graceful.GetManager().RunWithCancel(prAutoMergeQueue)
|
||||
go graceful.GetManager().RunWithCancel(automergequeue.AutoMergeQueue)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -56,24 +54,23 @@ func handler(items ...string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func addToQueue(pr *issues_model.PullRequest, sha string) {
|
||||
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
|
||||
if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
|
||||
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
|
||||
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) {
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil {
|
||||
return err
|
||||
}
|
||||
scheduled = true
|
||||
|
||||
_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
|
||||
return err
|
||||
})
|
||||
// Old code made "scheduled" to be true after "ScheduleAutoMerge", but it's not right:
|
||||
// If the transaction rolls back, then the pull request is not scheduled to auto merge.
|
||||
// So we should only set "scheduled" to true if there is no error.
|
||||
scheduled = err == nil
|
||||
if scheduled {
|
||||
log.Trace("Pull request [%d] scheduled for auto merge with style [%s] and message [%s]", pull.ID, style, message)
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, pull)
|
||||
}
|
||||
return scheduled, err
|
||||
}
|
||||
|
||||
@@ -99,38 +96,12 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m
|
||||
}
|
||||
|
||||
for _, pr := range pulls {
|
||||
addToQueue(pr, sha)
|
||||
automergequeue.AddToQueue(pr, sha)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
|
||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
addToQueue(pull, commitID)
|
||||
}
|
||||
|
||||
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
@@ -45,7 +46,7 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
|
||||
return
|
||||
}
|
||||
// as reviews could have blocked a pending automerge let's recheck
|
||||
StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
|
||||
}
|
||||
|
||||
func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
|
||||
|
||||
49
services/automergequeue/automergequeue.go
Normal file
49
services/automergequeue/automergequeue.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package automergequeue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
)
|
||||
|
||||
var AutoMergeQueue *queue.WorkerPoolQueue[string]
|
||||
|
||||
var AddToQueue = func(pr *issues_model.PullRequest, sha string) {
|
||||
log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
|
||||
if err := AutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
|
||||
log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
|
||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
|
||||
if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
AddToQueue(pull, commitID)
|
||||
}
|
||||
@@ -33,8 +33,8 @@ func (p *Pagination) WithCurRows(n int) *Pagination {
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Pagination) AddParamFromRequest(req *http.Request) {
|
||||
for key, values := range req.URL.Query() {
|
||||
func (p *Pagination) AddParamFromQuery(q url.Values) {
|
||||
for key, values := range q {
|
||||
if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") {
|
||||
continue
|
||||
}
|
||||
@@ -45,6 +45,10 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pagination) AddParamFromRequest(req *http.Request) {
|
||||
p.AddParamFromQuery(req.URL.Query())
|
||||
}
|
||||
|
||||
// GetParams returns the configured URL params
|
||||
func (p *Pagination) GetParams() template.URL {
|
||||
return template.URL(strings.Join(p.urlParams, "&"))
|
||||
|
||||
@@ -28,7 +28,6 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
// Deadline is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
|
||||
if ctx.Override != nil {
|
||||
return ctx.Override.Deadline()
|
||||
@@ -36,7 +35,6 @@ func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return ctx.Base.Deadline()
|
||||
}
|
||||
|
||||
// Done is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *PrivateContext) Done() <-chan struct{} {
|
||||
if ctx.Override != nil {
|
||||
return ctx.Override.Done()
|
||||
@@ -44,7 +42,6 @@ func (ctx *PrivateContext) Done() <-chan struct{} {
|
||||
return ctx.Base.Done()
|
||||
}
|
||||
|
||||
// Err is part of the interface for context.Context and we pass this to the request context
|
||||
func (ctx *PrivateContext) Err() error {
|
||||
if ctx.Override != nil {
|
||||
return ctx.Override.Err()
|
||||
@@ -52,14 +49,14 @@ func (ctx *PrivateContext) Err() error {
|
||||
return ctx.Base.Err()
|
||||
}
|
||||
|
||||
var privateContextKey any = "default_private_context"
|
||||
type privateContextKeyType struct{}
|
||||
|
||||
var privateContextKey privateContextKeyType
|
||||
|
||||
// GetPrivateContext returns a context for Private routes
|
||||
func GetPrivateContext(req *http.Request) *PrivateContext {
|
||||
return req.Context().Value(privateContextKey).(*PrivateContext)
|
||||
}
|
||||
|
||||
// PrivateContexter returns apicontext as middleware
|
||||
func PrivateContexter() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
@@ -143,7 +143,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
|
||||
mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
|
||||
approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
|
||||
|
||||
teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
|
||||
teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
|
||||
}
|
||||
@@ -485,7 +485,7 @@ func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo
|
||||
|
||||
whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
|
||||
|
||||
teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
|
||||
teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
RepoTransfer: transfer,
|
||||
Topics: util.SliceNilAsEmpty(repo.Topics),
|
||||
ObjectFormatName: repo.ObjectFormatName,
|
||||
Licenses: repoLicenses.StringList(),
|
||||
Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
|
||||
}
|
||||
|
||||
func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
|
||||
leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line)
|
||||
leftLine, leftHunk, rightLine, rightHunk := git.ParseDiffHunkString(line)
|
||||
|
||||
return &DiffLineSectionInfo{
|
||||
Path: treePath,
|
||||
@@ -188,7 +188,7 @@ func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int
|
||||
LeftIdx: leftLine,
|
||||
RightIdx: rightLine,
|
||||
LeftHunkSize: leftHunk,
|
||||
RightHunkSize: righHunk,
|
||||
RightHunkSize: rightHunk,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc
|
||||
// try to find equivalent diff line. ignore, otherwise
|
||||
switch diffLine.Type {
|
||||
case DiffLineSection:
|
||||
return getLineContent(diffLine.Content[1:], locale)
|
||||
return getLineContent(diffLine.Content, locale)
|
||||
case DiffLineAdd:
|
||||
compareDiffLine := diffSection.GetLine(diffLine.Match)
|
||||
return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale)
|
||||
@@ -904,6 +904,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
lastLeftIdx = -1
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
|
||||
// FIXME: the "-1" can't be right, these "line idx" are all 1-based, maybe there are other bugs that covers this bug.
|
||||
lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
|
||||
diffLine := &DiffLine{
|
||||
Type: DiffLineSection,
|
||||
@@ -1232,7 +1233,7 @@ func GetDiffForAPI(ctx context.Context, gitRepo *git.Repository, opts *DiffOptio
|
||||
return diff, err
|
||||
}
|
||||
|
||||
func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
|
||||
func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
|
||||
diff, beforeCommit, afterCommit, err := getDiffBasic(ctx, gitRepo, opts, files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1258,7 +1259,7 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp
|
||||
|
||||
// Populate Submodule URLs
|
||||
if diffFile.SubmoduleDiffInfo != nil {
|
||||
diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, afterCommit)
|
||||
diffFile.SubmoduleDiffInfo.PopulateURL(repoLink, diffFile, beforeCommit, afterCommit)
|
||||
}
|
||||
|
||||
if !isVendored.Has() {
|
||||
@@ -1359,6 +1360,7 @@ func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.
|
||||
// But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible
|
||||
if err != nil {
|
||||
log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err)
|
||||
err = nil //nolint
|
||||
}
|
||||
|
||||
filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState)
|
||||
@@ -1400,7 +1402,7 @@ outer:
|
||||
}
|
||||
}
|
||||
|
||||
return review, err
|
||||
return review, nil
|
||||
}
|
||||
|
||||
// CommentAsDiff returns c.Patch as *Diff
|
||||
|
||||
@@ -20,7 +20,7 @@ type SubmoduleDiffInfo struct {
|
||||
PreviousRefID string
|
||||
}
|
||||
|
||||
func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) {
|
||||
func (si *SubmoduleDiffInfo) PopulateURL(repoLink string, diffFile *DiffFile, leftCommit, rightCommit *git.Commit) {
|
||||
si.SubmoduleName = diffFile.Name
|
||||
submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit
|
||||
if diffFile.IsDeleted {
|
||||
@@ -30,18 +30,19 @@ func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCo
|
||||
return
|
||||
}
|
||||
|
||||
submodule, err := submoduleCommit.GetSubModule(diffFile.GetDiffFileName())
|
||||
submoduleFullPath := diffFile.GetDiffFileName()
|
||||
submodule, err := submoduleCommit.GetSubModule(submoduleFullPath)
|
||||
if err != nil {
|
||||
log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", diffFile.GetDiffFileName(), err)
|
||||
log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", submoduleFullPath, err)
|
||||
return // ignore the error, do not cause 500 errors for end users
|
||||
}
|
||||
if submodule != nil {
|
||||
si.SubmoduleFile = git.NewCommitSubmoduleFile(submodule.URL, submoduleCommit.ID.String())
|
||||
si.SubmoduleFile = git.NewCommitSubmoduleFile(repoLink, submoduleFullPath, submodule.URL, submoduleCommit.ID.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML {
|
||||
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, commitID)
|
||||
webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx, commitID)
|
||||
if webLink == nil {
|
||||
return htmlutil.HTMLFormat("%s", base.ShortSha(commitID))
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID s
|
||||
}
|
||||
|
||||
func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML {
|
||||
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, si.PreviousRefID, si.NewRefID)
|
||||
webLink := si.SubmoduleFile.SubmoduleWebLinkCompare(ctx, si.PreviousRefID, si.NewRefID)
|
||||
if webLink == nil {
|
||||
return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID))
|
||||
}
|
||||
@@ -57,7 +58,7 @@ func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.
|
||||
}
|
||||
|
||||
func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML {
|
||||
webLink := si.SubmoduleFile.SubmoduleWebLink(ctx)
|
||||
webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx)
|
||||
if webLink == nil {
|
||||
return htmlutil.HTMLFormat("%s", si.SubmoduleName)
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ func TestSubmoduleInfo(t *testing.T) {
|
||||
assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx))
|
||||
assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx))
|
||||
|
||||
sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234")
|
||||
sdi.SubmoduleFile = git.NewCommitSubmoduleFile("/any/repo-link", "fullpath", "https://github.com/owner/repo", "1234")
|
||||
assert.EqualValues(t, `<a href="https://github.com/owner/repo/tree/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
|
||||
assert.EqualValues(t, `<a href="https://github.com/owner/repo/compare/aaaa...bbbb">aaaa...bbbb</a>`, sdi.CompareRefIDLinkHTML(ctx))
|
||||
assert.EqualValues(t, `<a href="https://github.com/owner/repo">name</a>`, sdi.SubmoduleRepoLinkHTML(ctx))
|
||||
|
||||
@@ -304,7 +304,7 @@ func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, rep
|
||||
|
||||
// If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
|
||||
if repo.Owner.IsOrganization() {
|
||||
teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
|
||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
if err != nil {
|
||||
log.Error("GetTeamsWithAccessToRepo: %v", err)
|
||||
return false
|
||||
|
||||
@@ -322,7 +322,10 @@ func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *gith
|
||||
httpClient := NewMigrationHTTPClient()
|
||||
|
||||
for _, asset := range rel.Assets {
|
||||
assetID := *asset.ID // Don't optimize this, for closure we need a local variable
|
||||
assetID := asset.GetID() // Don't optimize this, for closure we need a local variable TODO: no need to do so in new Golang
|
||||
if assetID == 0 {
|
||||
continue
|
||||
}
|
||||
r.Assets = append(r.Assets, &base.ReleaseAsset{
|
||||
ID: asset.GetID(),
|
||||
Name: asset.GetName(),
|
||||
|
||||
@@ -46,10 +46,25 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
}
|
||||
}
|
||||
|
||||
func shouldSendCommentChangeNotification(ctx context.Context, comment *issues_model.Comment) bool {
|
||||
if err := comment.LoadReview(ctx); err != nil {
|
||||
log.Error("LoadReview: %v", err)
|
||||
return false
|
||||
} else if comment.Review != nil && comment.Review.Type == issues_model.ReviewTypePending {
|
||||
// Pending review comments updating should not triggered
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CreateIssueComment notifies issue comment related message to notifiers
|
||||
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
|
||||
issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
|
||||
) {
|
||||
if !shouldSendCommentChangeNotification(ctx, comment) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
notifier.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
|
||||
}
|
||||
@@ -156,6 +171,10 @@ func PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issue
|
||||
|
||||
// UpdateComment notifies update comment to notifiers
|
||||
func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
|
||||
if !shouldSendCommentChangeNotification(ctx, c) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
notifier.UpdateComment(ctx, doer, c, oldContent)
|
||||
}
|
||||
@@ -163,6 +182,10 @@ func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.C
|
||||
|
||||
// DeleteComment notifies delete comment to notifiers
|
||||
func DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) {
|
||||
if !shouldSendCommentChangeNotification(ctx, c) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
notifier.DeleteComment(ctx, doer, c)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Copyright 2019 The Gitea Authors.
|
||||
// All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/pull"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
@@ -238,7 +239,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
|
||||
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
|
||||
// and set to be either conflict or mergeable.
|
||||
func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) {
|
||||
// If status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
|
||||
// If the status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
|
||||
if pr.Status == issues_model.PullRequestStatusChecking {
|
||||
pr.Status = issues_model.PullRequestStatusMergeable
|
||||
}
|
||||
@@ -257,6 +258,16 @@ func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullReques
|
||||
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
|
||||
log.Error("Update[%-v]: %v", pr, err)
|
||||
}
|
||||
|
||||
// if there is a scheduled merge for this pull request, start the auto merge check (again)
|
||||
exist, _, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
|
||||
if err != nil {
|
||||
log.Error("GetScheduledMergeByPullID[%-v]: %v", pr, err)
|
||||
return
|
||||
} else if !exist {
|
||||
return
|
||||
}
|
||||
automergequeue.StartPRCheckAndAutoMerge(ctx, pr)
|
||||
}
|
||||
|
||||
// getMergeCommit checks if a pull request has been merged
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Copyright 2019 The Gitea Authors.
|
||||
// All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pull
|
||||
@@ -11,11 +10,18 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/pull"
|
||||
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/graceful"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPullRequest_AddToTaskQueue(t *testing.T) {
|
||||
@@ -63,6 +69,46 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
|
||||
|
||||
prPatchCheckerQueue.ShutdownWait(5 * time.Second)
|
||||
prPatchCheckerQueue.ShutdownWait(time.Second)
|
||||
prPatchCheckerQueue = nil
|
||||
}
|
||||
|
||||
func TestMarkPullRequestAsMergeable(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", func(items ...string) []string { return nil })
|
||||
go prPatchCheckerQueue.Run()
|
||||
defer func() {
|
||||
prPatchCheckerQueue.ShutdownWait(time.Second)
|
||||
prPatchCheckerQueue = nil
|
||||
}()
|
||||
|
||||
addToQueueShaChan := make(chan string, 1)
|
||||
defer test.MockVariableValue(&automergequeue.AddToQueue, func(pr *issues_model.PullRequest, sha string) {
|
||||
addToQueueShaChan <- sha
|
||||
})()
|
||||
ctx := t.Context()
|
||||
_, _ = db.GetEngine(ctx).ID(2).Update(&issues_model.PullRequest{Status: issues_model.PullRequestStatusChecking})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
require.False(t, pr.HasMerged)
|
||||
require.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
|
||||
|
||||
err := pull.ScheduleAutoMerge(ctx, &user_model.User{ID: 99999}, pr.ID, repo_model.MergeStyleMerge, "test msg", true)
|
||||
require.NoError(t, err)
|
||||
|
||||
exist, scheduleMerge, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.True(t, scheduleMerge.Doer.IsGhost())
|
||||
|
||||
markPullRequestAsMergeable(ctx, pr)
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
require.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
|
||||
|
||||
select {
|
||||
case sha := <-addToQueueShaChan:
|
||||
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", sha) // ref: refs/pull/3/head
|
||||
case <-time.After(1 * time.Second):
|
||||
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,15 +35,16 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
|
||||
}
|
||||
|
||||
for _, gp := range requiredContextsGlob {
|
||||
var targetStatus structs.CommitStatusState
|
||||
var targetStatuses []*git_model.CommitStatus
|
||||
for _, commitStatus := range commitStatuses {
|
||||
if gp.Match(commitStatus.Context) {
|
||||
targetStatus = commitStatus.State
|
||||
targetStatuses = append(targetStatuses, commitStatus)
|
||||
matchedCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
targetStatus := git_model.CalcCommitStatus(targetStatuses).State
|
||||
|
||||
// If required rule not match any action, then it is pending
|
||||
if targetStatus == "" {
|
||||
if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
|
||||
|
||||
@@ -30,6 +30,11 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
|
||||
{Context: "Build 2", State: structs.CommitStatusSuccess},
|
||||
{Context: "Build 2t", State: structs.CommitStatusFailure},
|
||||
},
|
||||
{
|
||||
{Context: "Build 1", State: structs.CommitStatusSuccess},
|
||||
{Context: "Build 2", State: structs.CommitStatusSuccess},
|
||||
{Context: "Build 2t", State: structs.CommitStatusFailure},
|
||||
},
|
||||
{
|
||||
{Context: "Build 1", State: structs.CommitStatusSuccess},
|
||||
{Context: "Build 2", State: structs.CommitStatusSuccess},
|
||||
@@ -45,6 +50,7 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
|
||||
{"Build*"},
|
||||
{"Build*", "Build 2t*"},
|
||||
{"Build*", "Build 2t*"},
|
||||
{"Build*"},
|
||||
{"Build*", "Build 2t*", "Build 3*"},
|
||||
{"Build*", "Build *", "Build 2t*", "Build 1*"},
|
||||
}
|
||||
@@ -53,6 +59,7 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
|
||||
structs.CommitStatusSuccess,
|
||||
structs.CommitStatusPending,
|
||||
structs.CommitStatusFailure,
|
||||
structs.CommitStatusFailure,
|
||||
structs.CommitStatusPending,
|
||||
structs.CommitStatusSuccess,
|
||||
}
|
||||
|
||||
@@ -85,5 +85,5 @@ func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*orga
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
return organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||
}
|
||||
|
||||
@@ -158,34 +158,26 @@ func (node *TreeViewNode) sortLevel() int {
|
||||
return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1)
|
||||
}
|
||||
|
||||
func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
|
||||
func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
|
||||
node := &TreeViewNode{
|
||||
EntryName: entry.Name(),
|
||||
EntryMode: entryModeString(entry.Mode()),
|
||||
FullPath: path.Join(parentDir, entry.Name()),
|
||||
}
|
||||
|
||||
if entry.IsLink() {
|
||||
// TODO: symlink to a folder or a file, the icon differs
|
||||
target, err := entry.FollowLink()
|
||||
if err == nil {
|
||||
_ = target.IsDir()
|
||||
// if target.IsDir() { } else { }
|
||||
}
|
||||
}
|
||||
|
||||
if node.EntryIcon == "" {
|
||||
node.EntryIcon = fileicon.RenderEntryIcon(renderedIconPool, entry)
|
||||
// TODO: no open icon support yet
|
||||
// node.EntryIconOpen = fileicon.RenderEntryIconOpen(renderedIconPool, entry)
|
||||
entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry)
|
||||
node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
|
||||
if entryInfo.EntryMode.IsDir() {
|
||||
entryInfo.IsOpen = true
|
||||
node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
|
||||
}
|
||||
|
||||
if node.EntryMode == "commit" {
|
||||
if subModule, err := commit.GetSubModule(node.FullPath); err != nil {
|
||||
log.Error("GetSubModule: %v", err)
|
||||
} else if subModule != nil {
|
||||
submoduleFile := git.NewCommitSubmoduleFile(subModule.URL, entry.ID.String())
|
||||
webLink := submoduleFile.SubmoduleWebLink(ctx)
|
||||
submoduleFile := git.NewCommitSubmoduleFile(repoLink, node.FullPath, subModule.URL, entry.ID.String())
|
||||
webLink := submoduleFile.SubmoduleWebLinkTree(ctx)
|
||||
node.SubmoduleURL = webLink.CommitWebLink
|
||||
}
|
||||
}
|
||||
@@ -204,7 +196,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) {
|
||||
})
|
||||
}
|
||||
|
||||
func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||
func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -213,14 +205,14 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP
|
||||
subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/")
|
||||
nodes := make([]*TreeViewNode, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
node := newTreeViewNodeFromEntry(ctx, renderedIconPool, commit, treePath, entry)
|
||||
node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, commit, treePath, entry)
|
||||
nodes = append(nodes, node)
|
||||
if entry.IsDir() && subPathDirName == entry.Name() {
|
||||
subTreePath := treePath + "/" + node.EntryName
|
||||
if subTreePath[0] == '/' {
|
||||
subTreePath = subTreePath[1:]
|
||||
}
|
||||
subNodes, err := listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
|
||||
subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
|
||||
if err != nil {
|
||||
log.Error("listTreeNodes: %v", err)
|
||||
} else {
|
||||
@@ -232,10 +224,10 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func GetTreeViewNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||
func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
|
||||
entry, err := commit.GetTreeEntryByPath(treePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), treePath, subPath)
|
||||
return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ func TestGetTreeViewNodes(t *testing.T) {
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
defer ctx.Repo.GitRepo.Close()
|
||||
|
||||
curRepoLink := "/any/repo-link"
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
mockIconForFile := func(id string) template.HTML {
|
||||
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
||||
@@ -71,25 +72,30 @@ func TestGetTreeViewNodes(t *testing.T) {
|
||||
mockIconForFolder := func(id string) template.HTML {
|
||||
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
||||
}
|
||||
treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "")
|
||||
mockOpenIconForFolder := func(id string) template.HTML {
|
||||
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
|
||||
}
|
||||
treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*TreeViewNode{
|
||||
{
|
||||
EntryName: "docs",
|
||||
EntryMode: "tree",
|
||||
FullPath: "docs",
|
||||
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
||||
EntryName: "docs",
|
||||
EntryMode: "tree",
|
||||
FullPath: "docs",
|
||||
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
||||
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
|
||||
},
|
||||
}, treeNodes)
|
||||
|
||||
treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md")
|
||||
treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*TreeViewNode{
|
||||
{
|
||||
EntryName: "docs",
|
||||
EntryMode: "tree",
|
||||
FullPath: "docs",
|
||||
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
||||
EntryName: "docs",
|
||||
EntryMode: "tree",
|
||||
FullPath: "docs",
|
||||
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
|
||||
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
|
||||
Children: []*TreeViewNode{
|
||||
{
|
||||
EntryName: "README.md",
|
||||
@@ -101,7 +107,7 @@ func TestGetTreeViewNodes(t *testing.T) {
|
||||
},
|
||||
}, treeNodes)
|
||||
|
||||
treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "docs", "README.md")
|
||||
treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "docs", "README.md")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*TreeViewNode{
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<td class="author">
|
||||
<div class="tw-flex">
|
||||
{{$userName := .Author.Name}}
|
||||
{{if .User}}
|
||||
{{if and .User (gt .User.ID 0)}}
|
||||
{{if and .User.FullName DefaultShowFullName}}
|
||||
{{$userName = .User.FullName}}
|
||||
{{end}}
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
{{end}}
|
||||
<div id="diff-container">
|
||||
{{if $showFileTree}}
|
||||
{{$.FileIconPoolHTML}}
|
||||
<div id="diff-file-tree" class="tw-hidden not-mobile"></div>
|
||||
<script>
|
||||
if (diffTreeVisible) document.getElementById('diff-file-tree').classList.remove('tw-hidden');
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<span class="label-filter-exclude-info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
|
||||
<div class="divider"></div>
|
||||
<a class="item label-filter-query-default" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
|
||||
<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
||||
<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" "0"}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
||||
{{/* The logic here is not the same as the label selector in the issue sidebar.
|
||||
The one in the issue sidebar renders "repo labels | divider | org labels".
|
||||
Maybe the logic should be updated to be consistent.*/}}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" 0}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
|
||||
<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
|
||||
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" -1}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
|
||||
{{if .OpenMilestones}}
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
{{end}}
|
||||
{{range $att := $release.Attachments}}
|
||||
<li class="item">
|
||||
<a target="_blank" class="tw-flex-grow-[2] gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
|
||||
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
|
||||
<strong class="flex-text-inline">{{svg "octicon-package" 16 "download-icon"}}<span class="gt-ellipsis">{{$att.Name}}</span></strong>
|
||||
</a>
|
||||
<div class="attachment-right-info flex-text-inline">
|
||||
|
||||
@@ -63,13 +63,33 @@
|
||||
{{.Name}}
|
||||
</a>
|
||||
<div class="flex-item-body flex-text-block">
|
||||
{{/*FIXME: TEAM-UNIT-PERMISSION this display is not right, search the fixme keyword to see more details */}}
|
||||
{{svg "octicon-shield-lock"}}
|
||||
{{if eq .AccessMode 1}}{{ctx.Locale.Tr "repo.settings.collaboration.read"}}{{else if eq .AccessMode 2}}{{ctx.Locale.Tr "repo.settings.collaboration.write"}}{{else if eq .AccessMode 3}}{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}{{else if eq .AccessMode 4}}{{ctx.Locale.Tr "repo.settings.collaboration.owner"}}{{else}}{{ctx.Locale.Tr "repo.settings.collaboration.undefined"}}{{end}}
|
||||
{{if eq .AccessMode 0}}
|
||||
{{ctx.Locale.Tr "repo.settings.collaboration.per_unit"}}
|
||||
{{else if eq .AccessMode 1}}
|
||||
{{ctx.Locale.Tr "repo.settings.collaboration.read"}}
|
||||
{{else if eq .AccessMode 2}}
|
||||
{{ctx.Locale.Tr "repo.settings.collaboration.write"}}
|
||||
{{else if eq .AccessMode 3}}
|
||||
{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}
|
||||
{{else if eq .AccessMode 4}}
|
||||
{{ctx.Locale.Tr "repo.settings.collaboration.owner"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.settings.collaboration.undefined"}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{if or (eq .AccessMode 1) (eq .AccessMode 2)}}
|
||||
{{if or (eq .AccessMode 0) (eq .AccessMode 1) (eq .AccessMode 2)}}
|
||||
{{$first := true}}
|
||||
<div class="flex-item-body" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.change_team_permission_tip"}}">
|
||||
Sections: {{range $u, $unit := $.Units}}{{if and ($.Repo.UnitEnabled ctx $unit.Type) ($team.UnitEnabled ctx $unit.Type)}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{ctx.Locale.Tr $unit.NameKey}}{{end}}{{end}} {{if $first}}None{{end}}
|
||||
Units:
|
||||
{{range $u, $unit := $.Units}}
|
||||
{{- if and ($.Repo.UnitEnabled ctx $unit.Type) ($team.UnitEnabled ctx $unit.Type) -}}
|
||||
{{- Iif $first "" ", "}}{{ctx.Locale.Tr $unit.NameKey -}}
|
||||
{{- $first = false -}}
|
||||
{{- end -}}
|
||||
{{end}}
|
||||
{{if $first}}None{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
{{template "repo/latest_commit" .}}
|
||||
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
|
||||
</div>
|
||||
{{$.FileIconPoolHTML}}
|
||||
{{if .HasParentPath}}
|
||||
<a class="repo-file-line parent-link silenced" href="{{.BranchLink}}{{if .ParentPath}}{{PathEscapeSegments .ParentPath}}{{end}}">
|
||||
{{svg "octicon-file-directory-fill"}} ..
|
||||
{{index $.FileIcons ".."}} ..
|
||||
</a>
|
||||
{{end}}
|
||||
{{$.FileIconPoolHTML}}
|
||||
{{range $item := .Files}}
|
||||
<div class="repo-file-item">
|
||||
{{$entry := $item.Entry}}
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}">
|
||||
{{index $.FileIcons $entry.Name}}
|
||||
{{if $entry.IsSubModule}}
|
||||
{{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}}
|
||||
{{$submoduleLink := $submoduleFile.SubmoduleWebLinkTree ctx}}
|
||||
{{if $submoduleLink}}
|
||||
<a class="entry-name" href="{{$submoduleLink.RepoWebLink}}" title="{{$entry.Name}}">{{$entry.Name}}</a>
|
||||
@ <a class="text primary" href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a>
|
||||
|
||||
24
templates/swagger/v1_json.tmpl
generated
24
templates/swagger/v1_json.tmpl
generated
@@ -20327,6 +20327,25 @@
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"ActionWorkflowResponse": {
|
||||
"description": "ActionWorkflowResponse returns a ActionWorkflow",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"total_count": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"x-go-name": "TotalCount"
|
||||
},
|
||||
"workflows": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActionWorkflow"
|
||||
},
|
||||
"x-go-name": "Workflows"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"ActionWorkflowRun": {
|
||||
"description": "ActionWorkflowRun represents a WorkflowRun",
|
||||
"type": "object",
|
||||
@@ -27464,10 +27483,7 @@
|
||||
"ActionWorkflowList": {
|
||||
"description": "ActionWorkflowList",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ActionWorkflow"
|
||||
}
|
||||
"$ref": "#/definitions/ActionWorkflowResponse"
|
||||
}
|
||||
},
|
||||
"ActivityFeedsList": {
|
||||
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/tests"
|
||||
@@ -58,9 +60,12 @@ func TestAPIDownloadArchive(t *testing.T) {
|
||||
|
||||
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name))
|
||||
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest)
|
||||
|
||||
t.Run("GitHubStyle", testAPIDownloadArchiveGitHubStyle)
|
||||
t.Run("PrivateRepo", testAPIDownloadArchivePrivateRepo)
|
||||
}
|
||||
|
||||
func TestAPIDownloadArchive2(t *testing.T) {
|
||||
func testAPIDownloadArchiveGitHubStyle(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
@@ -95,7 +100,13 @@ func TestAPIDownloadArchive2(t *testing.T) {
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, bs, 382)
|
||||
|
||||
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name))
|
||||
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func testAPIDownloadArchivePrivateRepo(t *testing.T) {
|
||||
_ = repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{ID: 1, IsPrivate: true}, "is_private")
|
||||
MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/archive/master.zip"), http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/zipball/master"), http.StatusNotFound)
|
||||
_ = repo_model.UpdateRepoUnitPublicAccess(t.Context(), &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeCode, AnonymousAccessMode: perm.AccessModeRead})
|
||||
MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/archive/master.zip"), http.StatusOK)
|
||||
MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/zipball/master"), http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -488,40 +489,60 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
|
||||
}
|
||||
|
||||
func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns, protectedFilePatterns string) func(t *testing.T) {
|
||||
return doProtectBranchExt(ctx, branch, doProtectBranchOptions{
|
||||
UserToWhitelistPush: userToWhitelistPush,
|
||||
UserToWhitelistForcePush: userToWhitelistForcePush,
|
||||
UnprotectedFilePatterns: unprotectedFilePatterns,
|
||||
ProtectedFilePatterns: protectedFilePatterns,
|
||||
})
|
||||
}
|
||||
|
||||
type doProtectBranchOptions struct {
|
||||
UserToWhitelistPush, UserToWhitelistForcePush, UnprotectedFilePatterns, ProtectedFilePatterns string
|
||||
|
||||
StatusCheckPatterns []string
|
||||
}
|
||||
|
||||
func doProtectBranchExt(ctx APITestContext, ruleName string, opts doProtectBranchOptions) func(t *testing.T) {
|
||||
// We are going to just use the owner to set the protection.
|
||||
return func(t *testing.T) {
|
||||
csrf := GetUserCSRFToken(t, ctx.Session)
|
||||
|
||||
formData := map[string]string{
|
||||
"_csrf": csrf,
|
||||
"rule_name": branch,
|
||||
"unprotected_file_patterns": unprotectedFilePatterns,
|
||||
"protected_file_patterns": protectedFilePatterns,
|
||||
"rule_name": ruleName,
|
||||
"unprotected_file_patterns": opts.UnprotectedFilePatterns,
|
||||
"protected_file_patterns": opts.ProtectedFilePatterns,
|
||||
}
|
||||
|
||||
if userToWhitelistPush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistPush)
|
||||
if opts.UserToWhitelistPush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, opts.UserToWhitelistPush)
|
||||
assert.NoError(t, err)
|
||||
formData["whitelist_users"] = strconv.FormatInt(user.ID, 10)
|
||||
formData["enable_push"] = "whitelist"
|
||||
formData["enable_whitelist"] = "on"
|
||||
}
|
||||
|
||||
if userToWhitelistForcePush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, userToWhitelistForcePush)
|
||||
if opts.UserToWhitelistForcePush != "" {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, opts.UserToWhitelistForcePush)
|
||||
assert.NoError(t, err)
|
||||
formData["force_push_allowlist_users"] = strconv.FormatInt(user.ID, 10)
|
||||
formData["enable_force_push"] = "whitelist"
|
||||
formData["enable_force_push_allowlist"] = "on"
|
||||
}
|
||||
|
||||
if len(opts.StatusCheckPatterns) > 0 {
|
||||
formData["enable_status_check"] = "on"
|
||||
formData["status_check_contexts"] = strings.Join(opts.StatusCheckPatterns, "\n")
|
||||
}
|
||||
|
||||
// Send the request to update branch protection settings
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/edit", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), formData)
|
||||
ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// Check if master branch has been locked successfully
|
||||
// Check if the "master" branch has been locked successfully
|
||||
flashMsg := ctx.Session.GetCookieFlashMessage()
|
||||
assert.Equal(t, `Branch protection for rule "`+branch+`" has been updated.`, flashMsg.SuccessMsg)
|
||||
assert.Equal(t, `Branch protection for rule "`+ruleName+`" has been updated.`, flashMsg.SuccessMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,6 +708,10 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
|
||||
ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// automerge will merge immediately if the PR is mergeable and there is no "status check" because no status check also means "all checks passed"
|
||||
// so we must set up a status check to test the auto merge feature
|
||||
doProtectBranchExt(ctx, "protected", doProtectBranchOptions{StatusCheckPatterns: []string{"*"}})(t)
|
||||
|
||||
t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
|
||||
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
|
||||
t.Run("GenerateCommit", func(t *testing.T) {
|
||||
@@ -727,13 +752,13 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
|
||||
|
||||
// Cancel not existing auto merge
|
||||
ctx.ExpectedCode = http.StatusNotFound
|
||||
t.Run("CancelAutoMergePR", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
t.Run("CancelAutoMergePRNotExist", doAPICancelAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
// Add auto merge request
|
||||
ctx.ExpectedCode = http.StatusCreated
|
||||
t.Run("AutoMergePR", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
// Can not create schedule twice
|
||||
// Cannot create schedule twice
|
||||
ctx.ExpectedCode = http.StatusConflict
|
||||
t.Run("AutoMergePRTwice", doAPIAutoMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
|
||||
|
||||
|
||||
@@ -135,3 +135,105 @@ func TestAgitPullPush(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAgitReviewStaleness(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
u.Path = baseAPITestContext.GitPath()
|
||||
u.User = url.UserPassword("user2", userPassword)
|
||||
|
||||
dstPath := t.TempDir()
|
||||
doGitClone(dstPath, u)(t)
|
||||
|
||||
gitRepo, err := git.OpenRepository(t.Context(), dstPath)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
doGitCreateBranch(dstPath, "test-agit-review")
|
||||
|
||||
// Create initial commit
|
||||
_, err = generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "initial-")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create PR via agit
|
||||
err = git.NewCommand("push", "origin",
|
||||
"-o", "title=Test agit Review Staleness", "-o", "description=Testing review staleness",
|
||||
"HEAD:refs/for/master/test-agit-review",
|
||||
).Run(git.DefaultContext, &git.RunOpts{Dir: dstPath})
|
||||
assert.NoError(t, err)
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
||||
BaseRepoID: 1,
|
||||
Flow: issues_model.PullRequestFlowAGit,
|
||||
HeadBranch: "user2/test-agit-review",
|
||||
})
|
||||
assert.NoError(t, pr.LoadIssue(db.DefaultContext))
|
||||
|
||||
// Get initial commit ID for the review
|
||||
initialCommitID := pr.HeadCommitID
|
||||
t.Logf("Initial commit ID: %s", initialCommitID)
|
||||
|
||||
// Create a review on the PR (as user1 reviewing user2's PR)
|
||||
reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
|
||||
Type: issues_model.ReviewTypeApprove,
|
||||
Reviewer: reviewer,
|
||||
Issue: pr.Issue,
|
||||
CommitID: initialCommitID,
|
||||
Content: "LGTM! Looks good to merge.",
|
||||
Official: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, review.Stale, "New review should not be stale")
|
||||
|
||||
// Verify review exists and is not stale
|
||||
reviews, err := issues_model.FindReviews(db.DefaultContext, issues_model.FindReviewOptions{
|
||||
IssueID: pr.IssueID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviews, 1)
|
||||
assert.Equal(t, initialCommitID, reviews[0].CommitID)
|
||||
assert.False(t, reviews[0].Stale, "Review should not be stale initially")
|
||||
|
||||
// Create a new commit and update the agit PR
|
||||
_, err = generateCommitWithNewData(testFileSizeSmall, dstPath, "user2@example.com", "User Two", "updated-")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = git.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-review").Run(git.DefaultContext, &git.RunOpts{Dir: dstPath})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Reload PR to get updated commit ID
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
|
||||
BaseRepoID: 1,
|
||||
Flow: issues_model.PullRequestFlowAGit,
|
||||
HeadBranch: "user2/test-agit-review",
|
||||
})
|
||||
assert.NoError(t, pr.LoadIssue(db.DefaultContext))
|
||||
|
||||
// For AGit PRs, HeadCommitID must be loaded from git references
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
baseGitRepo, err := gitrepo.OpenRepository(db.DefaultContext, baseRepo)
|
||||
assert.NoError(t, err)
|
||||
defer baseGitRepo.Close()
|
||||
|
||||
updatedCommitID, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName())
|
||||
assert.NoError(t, err)
|
||||
t.Logf("Updated commit ID: %s", updatedCommitID)
|
||||
|
||||
// Verify the PR was updated with new commit
|
||||
assert.NotEqual(t, initialCommitID, updatedCommitID, "PR should have new commit ID after update")
|
||||
|
||||
// Check that the review is now marked as stale
|
||||
reviews, err = issues_model.FindReviews(db.DefaultContext, issues_model.FindReviewOptions{
|
||||
IssueID: pr.IssueID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviews, 1)
|
||||
|
||||
assert.True(t, reviews[0].Stale, "Review should be marked as stale after AGit PR update")
|
||||
|
||||
// The review commit ID should remain the same (pointing to the original commit)
|
||||
assert.Equal(t, initialCommitID, reviews[0].CommitID, "Review commit ID should remain unchanged and point to original commit")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,12 +10,17 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOrgRepos(t *testing.T) {
|
||||
@@ -217,4 +222,32 @@ func TestTeamSearch(t *testing.T) {
|
||||
session = loginUser(t, user5.Name)
|
||||
req = NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "team")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
t.Run("SearchWithPermission", func(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
const testOrgID int64 = 500
|
||||
const testRepoID int64 = 2000
|
||||
testTeam := &organization.Team{OrgID: testOrgID, LowerName: "test_team", AccessMode: perm.AccessModeNone}
|
||||
require.NoError(t, db.Insert(ctx, testTeam))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: testOrgID, TeamID: testTeam.ID, RepoID: testRepoID}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: testOrgID, TeamID: testTeam.ID, Type: unit.TypeCode, AccessMode: perm.AccessModeRead}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: testOrgID, TeamID: testTeam.ID, Type: unit.TypeIssues, AccessMode: perm.AccessModeWrite}))
|
||||
|
||||
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeRead, unit.TypeCode, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, teams, 1) // can read "code" or "issues"
|
||||
|
||||
teams, err = organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeWrite, unit.TypeCode)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, teams) // cannot write "code"
|
||||
|
||||
teams, err = organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeWrite, unit.TypeIssues)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, teams, 1) // can write "issues"
|
||||
|
||||
_, _ = db.GetEngine(ctx).ID(testTeam.ID).Update(&organization.Team{AccessMode: perm.AccessModeWrite})
|
||||
teams, err = organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeWrite, unit.TypeCode)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, teams, 1) // team permission is "write", so can write "code"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
"code.gitea.io/gitea/services/automergequeue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
||||
@@ -726,7 +727,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
||||
|
||||
// add protected branch for commit status
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
// Change master branch to protected
|
||||
// Change the "master" branch to "protected"
|
||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
||||
"_csrf": csrf,
|
||||
"rule_name": "master",
|
||||
@@ -736,10 +737,22 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
oldAutoMergeAddToQueue := automergequeue.AddToQueue
|
||||
addToQueueShaChan := make(chan string, 1)
|
||||
automergequeue.AddToQueue = func(pr *issues_model.PullRequest, sha string) {
|
||||
addToQueueShaChan <- sha
|
||||
}
|
||||
// first time insert automerge record, return true
|
||||
scheduled, err := automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, scheduled)
|
||||
// and the pr should be added to automergequeue, in case it is already "mergeable"
|
||||
select {
|
||||
case <-addToQueueShaChan:
|
||||
case <-time.After(time.Second):
|
||||
assert.FailNow(t, "Timeout: nothing was added to automergequeue")
|
||||
}
|
||||
automergequeue.AddToQueue = oldAutoMergeAddToQueue
|
||||
|
||||
// second time insert automerge record, return false because it does exist
|
||||
scheduled, err = automerge.ScheduleAutoMerge(db.DefaultContext, user1, pr, repo_model.MergeStyleMerge, "auto merge test", false)
|
||||
@@ -774,13 +787,11 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) {
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// realod pr again
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
||||
assert.True(t, pr.HasMerged)
|
||||
assert.Eventually(t, func() bool {
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
|
||||
return pr.HasMerged
|
||||
}, 2*time.Second, 100*time.Millisecond)
|
||||
assert.NotEmpty(t, pr.MergedCommitID)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &pull_model.AutoMerge{PullID: pr.ID})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,39 +23,59 @@ import (
|
||||
|
||||
func TestRepoCommits(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
// Request repository commits page
|
||||
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
t.Run("CommitList", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
}
|
||||
|
||||
func Test_ReposGitCommitListNotMaster(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequest(t, "GET", "/user2/repo16/commits/branch/master")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
var commits []string
|
||||
doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) {
|
||||
commitURL, _ := s.Attr("href")
|
||||
commits = append(commits, path.Base(commitURL))
|
||||
var commits, userHrefs []string
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
doc.doc.Find("#commits-table .commit-id-short").Each(func(i int, s *goquery.Selection) {
|
||||
commits = append(commits, path.Base(s.AttrOr("href", "")))
|
||||
})
|
||||
doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) {
|
||||
userHrefs = append(userHrefs, s.AttrOr("href", ""))
|
||||
})
|
||||
assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
|
||||
assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs)
|
||||
})
|
||||
assert.Equal(t, []string{"69554a64c1e6030f051e5c3f94bfbd773cd6a324", "27566bd5738fc8b4e3fef3c5e72cce608537bd95", "5099b81332712fe655e34e8dd63574f503f61811"}, commits)
|
||||
|
||||
var userHrefs []string
|
||||
doc.doc.Find("#commits-table .author-wrapper").Each(func(i int, s *goquery.Selection) {
|
||||
userHref, _ := s.Attr("href")
|
||||
userHrefs = append(userHrefs, userHref)
|
||||
t.Run("LastCommit", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo16")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
|
||||
authorHref := doc.doc.Find(".latest-commit .author-wrapper").AttrOr("href", "")
|
||||
assert.Equal(t, "/user2/repo16/commit/69554a64c1e6030f051e5c3f94bfbd773cd6a324", commitHref)
|
||||
assert.Equal(t, "/user2", authorHref)
|
||||
})
|
||||
|
||||
t.Run("CommitListNonExistingCommiter", func(t *testing.T) {
|
||||
// check the commit list for a repository with no gitea user
|
||||
// * commit 985f0301dba5e7b34be866819cd15ad3d8f508ee (branch2)
|
||||
// * Author: 6543 <6543@obermui.de>
|
||||
req := NewRequest(t, "GET", "/user2/repo1/commits/branch/branch2")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
commitHref := doc.doc.Find("#commits-table tr:first-child .commit-id-short").AttrOr("href", "")
|
||||
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
|
||||
authorElem := doc.doc.Find("#commits-table tr:first-child .author-wrapper")
|
||||
assert.Equal(t, "6543", authorElem.Text())
|
||||
assert.Equal(t, "span", authorElem.Nodes[0].Data)
|
||||
})
|
||||
|
||||
t.Run("LastCommitNonExistingCommiter", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/repo1/src/branch/branch2")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
commitHref := doc.doc.Find(".latest-commit .commit-id-short").AttrOr("href", "")
|
||||
assert.Equal(t, "/user2/repo1/commit/985f0301dba5e7b34be866819cd15ad3d8f508ee", commitHref)
|
||||
authorElem := doc.doc.Find(".latest-commit .author-wrapper")
|
||||
assert.Equal(t, "6543", authorElem.Text())
|
||||
assert.Equal(t, "span", authorElem.Nodes[0].Data)
|
||||
})
|
||||
assert.Equal(t, []string{"/user2", "/user21", "/user2"}, userHrefs)
|
||||
}
|
||||
|
||||
func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
|
||||
|
||||
@@ -71,11 +71,13 @@
|
||||
|
||||
#git-graph-container li .author {
|
||||
color: var(--color-text-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#git-graph-container li .time {
|
||||
color: var(--color-text-light-3);
|
||||
font-size: 80%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#git-graph-container li a:not(.ui):hover {
|
||||
@@ -109,6 +111,10 @@
|
||||
background-color: var(--color-secondary-alpha-30);
|
||||
}
|
||||
|
||||
#git-graph-container #rev-list .commit-refs {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#git-graph-container #rev-list .commit-refs .button {
|
||||
padding: 2px 4px;
|
||||
margin-right: 0.25em;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
flex-wrap: nowrap;
|
||||
overflow: auto;
|
||||
margin: 0 0.5em;
|
||||
min-height: max(calc(100vh - 400px), 300px);
|
||||
max-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
@@ -70,7 +71,7 @@
|
||||
.card-attachment-images {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
cursor: default;
|
||||
scroll-snap-type: x mandatory;
|
||||
text-align: center;
|
||||
@@ -84,6 +85,7 @@
|
||||
scroll-snap-align: center;
|
||||
margin-right: 2px;
|
||||
aspect-ratio: 1;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.card-attachment-images img:only-child {
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
#repo-files-table .repo-file-cell.name .entry-name {
|
||||
flex-shrink: 1;
|
||||
min-width: 3em;
|
||||
min-width: 1ch; /* leave about one letter space when shrinking, need to fine tune the "shrinks" in this grid in the future */
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
|
||||
@@ -70,8 +70,12 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#release-list .release-entry .attachment-list > .item a {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
#release-list .release-entry .attachment-list .attachment-right-info {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export default defineComponent({
|
||||
return {
|
||||
tab,
|
||||
repos: [],
|
||||
reposTotalCount: 0,
|
||||
reposTotalCount: null,
|
||||
reposFilter,
|
||||
archivedFilter,
|
||||
privateFilter,
|
||||
@@ -112,9 +112,6 @@ export default defineComponent({
|
||||
const el = document.querySelector('#dashboard-repo-list');
|
||||
this.changeReposFilter(this.reposFilter);
|
||||
fomanticQuery(el.querySelector('.ui.dropdown')).dropdown();
|
||||
nextTick(() => {
|
||||
this.$refs.search?.focus();
|
||||
});
|
||||
|
||||
this.textArchivedFilterTitles = {
|
||||
'archived': this.textShowOnlyArchived,
|
||||
@@ -242,12 +239,20 @@ export default defineComponent({
|
||||
|
||||
let response, json;
|
||||
try {
|
||||
const firstLoad = this.reposTotalCount === null;
|
||||
if (!this.reposTotalCount) {
|
||||
const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
|
||||
response = await GET(totalCountSearchURL);
|
||||
this.reposTotalCount = parseInt(response.headers.get('X-Total-Count') ?? '0');
|
||||
}
|
||||
|
||||
if (firstLoad && this.reposTotalCount) {
|
||||
nextTick(() => {
|
||||
// MDN: If there's no focused element, this is the Document.body or Document.documentElement.
|
||||
if ((document.activeElement === document.body || document.activeElement === document.documentElement)) {
|
||||
this.$refs.search.focus({preventScroll: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
response = await GET(searchedURL);
|
||||
json = await response.json();
|
||||
} catch {
|
||||
@@ -349,7 +354,7 @@ export default defineComponent({
|
||||
<h4 class="ui top attached header tw-flex tw-items-center">
|
||||
<div class="tw-flex-1 tw-flex tw-items-center">
|
||||
{{ textMyRepos }}
|
||||
<span class="ui grey label tw-ml-2">{{ reposTotalCount }}</span>
|
||||
<span v-if="reposTotalCount" class="ui grey label tw-ml-2">{{ reposTotalCount }}</span>
|
||||
</div>
|
||||
<a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo">
|
||||
<svg-icon name="octicon-plus"/>
|
||||
@@ -420,7 +425,7 @@ export default defineComponent({
|
||||
</div>
|
||||
<div v-if="repos.length" class="ui attached table segment tw-rounded-b">
|
||||
<ul class="repo-owner-name-list">
|
||||
<li class="tw-flex tw-items-center tw-py-2" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
|
||||
<li class="tw-flex tw-items-center tw-py-2" v-for="(repo, index) in repos" :class="{'active': index === activeIndex}" :key="repo.id">
|
||||
<a class="repo-list-link muted" :href="repo.link">
|
||||
<svg-icon :name="repoIcon(repo)" :size="16" class="repo-list-icon"/>
|
||||
<div class="text truncate">{{ repo.full_name }}</div>
|
||||
|
||||
@@ -22,13 +22,6 @@ function getIconForDiffStatus(pType: DiffStatus) {
|
||||
};
|
||||
return diffTypes[pType] ?? diffTypes[''];
|
||||
}
|
||||
|
||||
function entryIcon(entry: DiffTreeEntry) {
|
||||
if (entry.EntryMode === 'commit') {
|
||||
return 'octicon-file-submodule';
|
||||
}
|
||||
return 'octicon-file';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -36,10 +29,8 @@ function entryIcon(entry: DiffTreeEntry) {
|
||||
<div class="item-directory" :class="{ 'viewed': item.IsViewed }" :title="item.DisplayName" @click.stop="collapsed = !collapsed">
|
||||
<!-- directory -->
|
||||
<SvgIcon :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'"/>
|
||||
<SvgIcon
|
||||
class="text primary"
|
||||
:name="collapsed ? 'octicon-file-directory-fill' : 'octicon-file-directory-open-fill'"
|
||||
/>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="tw-contents" v-html="collapsed ? store.folderIcon : store.folderOpenIcon"/>
|
||||
<span class="gt-ellipsis">{{ item.DisplayName }}</span>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +44,8 @@ function entryIcon(entry: DiffTreeEntry) {
|
||||
:title="item.DisplayName" :href="'#diff-' + item.NameHash"
|
||||
>
|
||||
<!-- file -->
|
||||
<SvgIcon :name="entryIcon(item)"/>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span class="tw-contents" v-html="item.FileIcon"/>
|
||||
<span class="gt-ellipsis tw-flex-1">{{ item.DisplayName }}</span>
|
||||
<SvgIcon
|
||||
:name="getIconForDiffStatus(item.DiffStatus).name"
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import {assignElementProperty} from './common-button.ts';
|
||||
import {assignElementProperty, type ElementWithAssignableProperties} from './common-button.ts';
|
||||
|
||||
test('assignElementProperty', () => {
|
||||
const elForm = document.createElement('form');
|
||||
assignElementProperty(elForm, 'action', '/test-link');
|
||||
expect(elForm.action).contains('/test-link'); // the DOM always returns absolute URL
|
||||
expect(elForm.getAttribute('action')).eq('/test-link');
|
||||
assignElementProperty(elForm, 'text-content', 'dummy');
|
||||
expect(elForm.textContent).toBe('dummy');
|
||||
|
||||
// mock a form with its property "action" overwritten by an input element
|
||||
const elFormWithAction = new class implements ElementWithAssignableProperties {
|
||||
action = document.createElement('input'); // now "form.action" is not string, but an input element
|
||||
_attrs: Record<string, string> = {};
|
||||
setAttribute(name: string, value: string) { this._attrs[name] = value }
|
||||
getAttribute(name: string): string | null { return this._attrs[name] }
|
||||
}();
|
||||
assignElementProperty(elFormWithAction, 'action', '/bar');
|
||||
expect(elFormWithAction.getAttribute('action')).eq('/bar');
|
||||
|
||||
const elInput = document.createElement('input');
|
||||
expect(elInput.readOnly).toBe(false);
|
||||
assignElementProperty(elInput, 'read-only', 'true');
|
||||
|
||||
@@ -102,18 +102,26 @@ function onHidePanelClick(el: HTMLElement, e: MouseEvent) {
|
||||
throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code
|
||||
}
|
||||
|
||||
export function assignElementProperty(el: any, name: string, val: string) {
|
||||
name = camelize(name);
|
||||
const old = el[name];
|
||||
export type ElementWithAssignableProperties = {
|
||||
getAttribute: (name: string) => string | null;
|
||||
setAttribute: (name: string, value: string) => void;
|
||||
} & Record<string, any>
|
||||
|
||||
export function assignElementProperty(el: ElementWithAssignableProperties, kebabName: string, val: string) {
|
||||
const camelizedName = camelize(kebabName);
|
||||
const old = el[camelizedName];
|
||||
if (typeof old === 'boolean') {
|
||||
el[name] = val === 'true';
|
||||
el[camelizedName] = val === 'true';
|
||||
} else if (typeof old === 'number') {
|
||||
el[name] = parseFloat(val);
|
||||
el[camelizedName] = parseFloat(val);
|
||||
} else if (typeof old === 'string') {
|
||||
el[name] = val;
|
||||
el[camelizedName] = val;
|
||||
} else if (old?.nodeName) {
|
||||
// "form" has an edge case: its "<input name=action>" element overwrites the "action" property, we can only set attribute
|
||||
el.setAttribute(kebabName, val);
|
||||
} else {
|
||||
// in the future, we could introduce a better typing system like `data-modal-form.action:string="..."`
|
||||
throw new Error(`cannot assign element property ${name} by value ${val}`);
|
||||
throw new Error(`cannot assign element property "${camelizedName}" by value "${val}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,9 +84,9 @@ export function initRepoGraphGit() {
|
||||
|
||||
const flowSelectRefsDropdown = document.querySelector('#flow-select-refs-dropdown');
|
||||
const $dropdown = fomanticQuery(flowSelectRefsDropdown);
|
||||
$dropdown.dropdown({clearable: true});
|
||||
$dropdown.dropdown('set selected', dropdownSelected);
|
||||
$dropdown.dropdown({
|
||||
clearable: true,
|
||||
fullTextSeach: 'exact',
|
||||
onRemove(toRemove: string) {
|
||||
if (toRemove === '...flow-hide-pr-refs') {
|
||||
params.delete('hide-pr-refs');
|
||||
@@ -110,7 +110,6 @@ export function initRepoGraphGit() {
|
||||
updateGraph();
|
||||
},
|
||||
});
|
||||
$dropdown.dropdown('set selected', dropdownSelected);
|
||||
|
||||
graphContainer.addEventListener('mouseenter', (e: DOMEvent<MouseEvent>) => {
|
||||
if (e.target.matches('#rev-list li')) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {minimatch} from 'minimatch';
|
||||
import {createMonaco} from './codeeditor.ts';
|
||||
import {onInputDebounce, queryElems, toggleClass, toggleElem} from '../utils/dom.ts';
|
||||
import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
@@ -124,14 +124,18 @@ function initRepoSettingsOptions() {
|
||||
const pageContent = document.querySelector('.page-content.repository.settings.options');
|
||||
if (!pageContent) return;
|
||||
|
||||
// Enable or select internal/external wiki system and issue tracker.
|
||||
// toggle related panels for the checkbox/radio inputs, the "selector" may not exist
|
||||
const toggleTargetContextPanel = (selector: string, enabled: boolean) => {
|
||||
if (!selector) return;
|
||||
queryElems(document, selector, (el) => el.classList.toggle('disabled', !enabled));
|
||||
};
|
||||
queryElems<HTMLInputElement>(pageContent, '.enable-system', (el) => el.addEventListener('change', () => {
|
||||
toggleClass(el.getAttribute('data-target'), 'disabled', !el.checked);
|
||||
toggleClass(el.getAttribute('data-context'), 'disabled', el.checked);
|
||||
toggleTargetContextPanel(el.getAttribute('data-target'), el.checked);
|
||||
toggleTargetContextPanel(el.getAttribute('data-context'), !el.checked);
|
||||
}));
|
||||
queryElems<HTMLInputElement>(pageContent, '.enable-system-radio', (el) => el.addEventListener('change', () => {
|
||||
toggleClass(el.getAttribute('data-target'), 'disabled', el.value === 'false');
|
||||
toggleClass(el.getAttribute('data-context'), 'disabled', el.value === 'true');
|
||||
toggleTargetContextPanel(el.getAttribute('data-target'), el.value === 'true');
|
||||
toggleTargetContextPanel(el.getAttribute('data-context'), el.value === 'false');
|
||||
}));
|
||||
|
||||
queryElems<HTMLInputElement>(pageContent, '.js-tracker-issue-style', (el) => el.addEventListener('change', () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ test('diff-tree', () => {
|
||||
'IsViewed': false,
|
||||
'NameHash': '....',
|
||||
'DiffStatus': '',
|
||||
'FileIcon': '',
|
||||
'Children': [
|
||||
{
|
||||
'FullName': 'dir1',
|
||||
@@ -17,6 +18,7 @@ test('diff-tree', () => {
|
||||
'IsViewed': false,
|
||||
'NameHash': '....',
|
||||
'DiffStatus': '',
|
||||
'FileIcon': '',
|
||||
'Children': [
|
||||
{
|
||||
'FullName': 'dir1/test.txt',
|
||||
@@ -25,6 +27,7 @@ test('diff-tree', () => {
|
||||
'NameHash': '....',
|
||||
'EntryMode': '',
|
||||
'IsViewed': false,
|
||||
'FileIcon': '',
|
||||
'Children': null,
|
||||
},
|
||||
],
|
||||
@@ -36,11 +39,12 @@ test('diff-tree', () => {
|
||||
'DiffStatus': 'added',
|
||||
'EntryMode': '',
|
||||
'IsViewed': false,
|
||||
'FileIcon': '',
|
||||
'Children': null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}, '', '');
|
||||
diffTreeStoreSetViewed(store, 'dir1/test.txt', true);
|
||||
expect(store.fullNameMap['dir1/test.txt'].IsViewed).toBe(true);
|
||||
expect(store.fullNameMap['dir1'].IsViewed).toBe(true);
|
||||
|
||||
@@ -13,7 +13,7 @@ export type DiffTreeEntry = {
|
||||
EntryMode: string,
|
||||
IsViewed: boolean,
|
||||
Children: DiffTreeEntry[],
|
||||
|
||||
FileIcon: string,
|
||||
ParentEntry?: DiffTreeEntry,
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ type DiffFileTreeData = {
|
||||
};
|
||||
|
||||
type DiffFileTree = {
|
||||
folderIcon: string;
|
||||
folderOpenIcon: string;
|
||||
diffFileTree: DiffFileTreeData;
|
||||
fullNameMap?: Record<string, DiffTreeEntry>
|
||||
fileTreeIsVisible: boolean;
|
||||
@@ -31,7 +33,7 @@ type DiffFileTree = {
|
||||
let diffTreeStoreReactive: Reactive<DiffFileTree>;
|
||||
export function diffTreeStore() {
|
||||
if (!diffTreeStoreReactive) {
|
||||
diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree);
|
||||
diffTreeStoreReactive = reactiveDiffTreeStore(pageData.DiffFileTree, pageData.FolderIcon, pageData.FolderOpenIcon);
|
||||
}
|
||||
return diffTreeStoreReactive;
|
||||
}
|
||||
@@ -55,9 +57,11 @@ function fillFullNameMap(map: Record<string, DiffTreeEntry>, entry: DiffTreeEntr
|
||||
}
|
||||
}
|
||||
|
||||
export function reactiveDiffTreeStore(data: DiffFileTreeData): Reactive<DiffFileTree> {
|
||||
export function reactiveDiffTreeStore(data: DiffFileTreeData, folderIcon: string, folderOpenIcon: string): Reactive<DiffFileTree> {
|
||||
const store = reactive({
|
||||
diffFileTree: data,
|
||||
folderIcon,
|
||||
folderOpenIcon,
|
||||
fileTreeIsVisible: false,
|
||||
selectedItem: '',
|
||||
fullNameMap: {},
|
||||
|
||||
Reference in New Issue
Block a user