Skip to content

Commit 998a431

Browse files
committed
Do not update PRs based on events that happened before they existed
* Split TestPullRequest out of AddTestPullRequestTask * A Created field is added to the Issue table * The Created field is set to the time (with nano resolution) on creation * Record the nano time repo_module.PushUpdateOptions is created by the hook * The decision to update a pull request created before a commit was pushed is based on the time (with nano resolution) the git hook was run and the Created field It ensures the following happens: * commit C is pushed * the git hook queues AddTestPullRequestTask for processing and returns with success * TestPullRequest is not called yet * a pull request P with commit C as the head is created * TestPullRequest runs and ignores P because it was created after the commit was received When the "created" column is NULL, no verification is done, pull requests that were created before the column was created in the database cannot be newer than the latest call to a git hook. Fixes: https://codeberg.org/forgejo/forgejo/issues/2009
1 parent f1c1a1e commit 998a431

17 files changed

Lines changed: 414 additions & 90 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-
2+
id: 1001
3+
repo_id: 1
4+
index: 1001
5+
poster_id: 1
6+
name: issue1
7+
content: content for the first issue
8+
is_pull: true
9+
created: 111111111
10+
created_unix: 946684800
11+
updated_unix: 978307200
12+
is_closed: false
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-
2+
id: 1001
3+
type: 0 # pull request
4+
status: 2 # mergable
5+
issue_id: 1001
6+
index: 1001
7+
head_repo_id: 1
8+
base_repo_id: 1
9+
head_branch: branchmax
10+
base_branch: master
11+
merge_base: 4a357436d925b5c974181ff12a994538ddc5a269
12+
has_merged: false
13+
flow: 0

models/forgejo_migrations/migrate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ var migrations = []*Migration{
5959
// v9 -> v10
6060
NewMigration("Add pronouns to user", forgejo_v1_22.AddPronounsToUser),
6161
// v11 -> v12
62-
// NewMigration()
62+
NewMigration("Add the `created` column to the `issue` table", forgejo_v1_22.AddCreatedToIssue),
6363
// v12 -> v13
6464
NewMigration("Add repo_archive_download_count table", forgejo_v1_22.AddRepoArchiveDownloadCount),
6565
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2024 The Forgejo Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_22 //nolint
5+
6+
import (
7+
"code.gitea.io/gitea/modules/timeutil"
8+
9+
"xorm.io/xorm"
10+
)
11+
12+
func AddCreatedToIssue(x *xorm.Engine) error {
13+
type Issue struct {
14+
ID int64 `xorm:"pk autoincr"`
15+
Created timeutil.TimeStampNano
16+
}
17+
18+
return x.Sync(&Issue{})
19+
}

models/issues/issue.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ type Issue struct {
124124

125125
DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"`
126126

127+
Created timeutil.TimeStampNano
128+
127129
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
128130
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
129131
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`

models/issues/issue_index.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import (
99
"code.gitea.io/gitea/models/db"
1010
)
1111

12+
func GetMaxIssueIndexForRepo(ctx context.Context, repoID int64) (int64, error) {
13+
var max int64
14+
if _, err := db.GetEngine(ctx).Select("MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
15+
return 0, err
16+
}
17+
return max, nil
18+
}
19+
1220
// RecalculateIssueIndexForRepo create issue_index for repo if not exist and
1321
// update it based on highest index of existing issues assigned to a repo
1422
func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
@@ -18,8 +26,8 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
1826
}
1927
defer committer.Close()
2028

21-
var max int64
22-
if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
29+
max, err := GetMaxIssueIndexForRepo(ctx, repoID)
30+
if err != nil {
2331
return err
2432
}
2533

models/issues/issue_index_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2024 The Forgejo Authors
2+
// SPDX-License-Identifier: MIT
3+
4+
package issues_test
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/db"
10+
issues_model "code.gitea.io/gitea/models/issues"
11+
repo_model "code.gitea.io/gitea/models/repo"
12+
"code.gitea.io/gitea/models/unittest"
13+
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestGetMaxIssueIndexForRepo(t *testing.T) {
18+
assert.NoError(t, unittest.PrepareTestDatabase())
19+
20+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
21+
22+
maxPR, err := issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
23+
assert.NoError(t, err)
24+
25+
issue := testCreateIssue(t, repo.ID, repo.OwnerID, "title1", "content1", false)
26+
assert.Greater(t, issue.Index, maxPR)
27+
28+
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
29+
assert.NoError(t, err)
30+
31+
pull := testCreateIssue(t, repo.ID, repo.OwnerID, "title2", "content2", true)
32+
assert.Greater(t, pull.Index, maxPR)
33+
34+
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
35+
assert.NoError(t, err)
36+
37+
assert.Equal(t, maxPR, pull.Index)
38+
}

models/issues/issue_update.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
325325
return fmt.Errorf("issue exist")
326326
}
327327

328+
opts.Issue.Created = timeutil.TimeStampNanoNow()
329+
328330
if _, err := e.Insert(opts.Issue); err != nil {
329331
return err
330332
}

models/issues/pull_list.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
4747
return sess, nil
4848
}
4949

50+
func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, olderThan int64, branch string) ([]*PullRequest, error) {
51+
prs := make([]*PullRequest, 0, 2)
52+
sess := db.GetEngine(ctx).
53+
Join("INNER", "issue", "issue.id = `pull_request`.issue_id").
54+
Where("`pull_request`.head_repo_id = ? AND `pull_request`.head_branch = ? AND `pull_request`.has_merged = ? AND `issue`.is_closed = ? AND `pull_request`.flow = ? AND (`issue`.`created` IS NULL OR `issue`.`created` <= ?)", repoID, branch, false, false, PullRequestFlowGithub, olderThan)
55+
return prs, sess.Find(&prs)
56+
}
57+
5058
// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
5159
func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) {
5260
prs := make([]*PullRequest, 0, 2)

models/issues/pull_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
package issues_test
55

66
import (
7+
"fmt"
78
"testing"
9+
"time"
810

911
"code.gitea.io/gitea/models/db"
1012
issues_model "code.gitea.io/gitea/models/issues"
1113
repo_model "code.gitea.io/gitea/models/repo"
1214
"code.gitea.io/gitea/models/unittest"
1315
user_model "code.gitea.io/gitea/models/user"
1416
"code.gitea.io/gitea/modules/setting"
17+
"code.gitea.io/gitea/tests"
1518

1619
"github.com/stretchr/testify/assert"
1720
)
@@ -156,6 +159,100 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
156159
}
157160
}
158161

162+
func TestGetUnmergedPullRequestsByHeadInfoMax(t *testing.T) {
163+
defer tests.AddFixtures("models/fixtures/TestGetUnmergedPullRequestsByHeadInfoMax/")()
164+
assert.NoError(t, unittest.PrepareTestDatabase())
165+
166+
repoID := int64(1)
167+
olderThan := int64(0)
168+
169+
// for NULL created field the olderThan condition is ignored
170+
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, olderThan, "branch2")
171+
assert.NoError(t, err)
172+
assert.Equal(t, int64(1), prs[0].HeadRepoID)
173+
174+
// test for when the created field is set
175+
branch := "branchmax"
176+
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, olderThan, branch)
177+
assert.NoError(t, err)
178+
assert.Len(t, prs, 0)
179+
olderThan = time.Now().UnixNano()
180+
assert.NoError(t, err)
181+
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, olderThan, branch)
182+
assert.NoError(t, err)
183+
assert.Len(t, prs, 1)
184+
for _, pr := range prs {
185+
assert.Equal(t, int64(1), pr.HeadRepoID)
186+
assert.Equal(t, branch, pr.HeadBranch)
187+
}
188+
pr := prs[0]
189+
190+
for _, testCase := range []struct {
191+
table string
192+
field string
193+
id int64
194+
match any
195+
nomatch any
196+
}{
197+
{
198+
table: "issue",
199+
field: "is_closed",
200+
id: pr.IssueID,
201+
match: false,
202+
nomatch: true,
203+
},
204+
{
205+
table: "pull_request",
206+
field: "flow",
207+
id: pr.ID,
208+
match: issues_model.PullRequestFlowGithub,
209+
nomatch: issues_model.PullRequestFlowAGit,
210+
},
211+
{
212+
table: "pull_request",
213+
field: "head_repo_id",
214+
id: pr.ID,
215+
match: pr.HeadRepoID,
216+
nomatch: 0,
217+
},
218+
{
219+
table: "pull_request",
220+
field: "head_branch",
221+
id: pr.ID,
222+
match: pr.HeadBranch,
223+
nomatch: "something else",
224+
},
225+
{
226+
table: "pull_request",
227+
field: "has_merged",
228+
id: pr.ID,
229+
match: false,
230+
nomatch: true,
231+
},
232+
} {
233+
t.Run(testCase.field, func(t *testing.T) {
234+
update := fmt.Sprintf("UPDATE `%s` SET `%s` = ? WHERE `id` = ?", testCase.table, testCase.field)
235+
236+
// expect no match
237+
_, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.nomatch, testCase.id)
238+
assert.NoError(t, err)
239+
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, olderThan, branch)
240+
assert.NoError(t, err)
241+
assert.Len(t, prs, 0)
242+
243+
// expect one match
244+
_, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.match, testCase.id)
245+
assert.NoError(t, err)
246+
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, olderThan, branch)
247+
assert.NoError(t, err)
248+
assert.Len(t, prs, 1)
249+
250+
// identical to the known PR
251+
assert.Equal(t, pr.ID, prs[0].ID)
252+
})
253+
}
254+
}
255+
159256
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
160257
assert.NoError(t, unittest.PrepareTestDatabase())
161258
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master")

0 commit comments

Comments
 (0)