From 62f2d717b7e04565c5ff260746e43ed64a87b0e0 Mon Sep 17 00:00:00 2001 From: Mihir Joshi <mihir67mj@gmail.com> Date: Sun, 21 Jan 2024 19:48:37 +0530 Subject: [PATCH] Fix reverting a merge commit failing (#28794) (#28825) Backport https://github.com/go-gitea/gitea/pull/28794 Fixes #22236 --- Error occurring currently while trying to revert commit using read-tree -m approach: > 2022/12/26 16:04:43 ...rvices/pull/patch.go:240:AttemptThreeWayMerge() [E] [63a9c61a] Unable to run read-tree -m! Error: exit status 128 - fatal: this operation must be run in a work tree > - fatal: this operation must be run in a work tree We need to clone a non-bare repository for `git read-tree -m` to work. https://github.com/go-gitea/gitea/commit/bb371aee6ecf5e570cdf7b5f7f0d6f47a607a325 adds support to create a non-bare cloned temporary upload repository. After cloning a non-bare temporary upload repository, we [set default index](https://github.com/go-gitea/gitea/blob/main/services/repository/files/cherry_pick.go#L37) (`git read-tree HEAD`). This operation ends up resetting the git index file (see investigation details below), due to which, we need to call `git update-index --refresh` afterward. Here's the diff of the index file before and after we execute SetDefaultIndex: https://www.diffchecker.com/hyOP3eJy/ Notice the **ctime**, **mtime** are set to 0 after SetDefaultIndex. You can reproduce the same behavior using these steps: ```bash $ git clone https://try.gitea.io/me-heer/test.git -s -b main $ cd test $ git read-tree HEAD $ git read-tree -m 1f085d7ed8 1f085d7ed8 9933caed00 error: Entry '1' not uptodate. Cannot merge. ``` After which, we can fix like this: ```bash $ git update-index --refresh $ git read-tree -m 1f085d7ed8 1f085d7ed8 9933caed00 ``` --- models/fixtures/repo_unit.yml | 7 ++++ models/fixtures/repository.yml | 13 +++++++ models/fixtures/user.yml | 2 +- services/packages/cargo/index.go | 2 +- services/repository/files/cherry_pick.go | 5 ++- services/repository/files/diff.go | 2 +- services/repository/files/patch.go | 2 +- services/repository/files/temp_repo.go | 17 +++++++-- services/repository/files/update.go | 2 +- services/repository/files/upload.go | 2 +- .../user2/test_commit_revert.git/HEAD | 1 + .../user2/test_commit_revert.git/config | 8 +++++ .../user2/test_commit_revert.git/description | 1 + .../user2/test_commit_revert.git/info/exclude | 6 ++++ ...200c8e6707636a6cc3e0d8101fba08b19dcb91.idx | Bin 0 -> 1268 bytes ...00c8e6707636a6cc3e0d8101fba08b19dcb91.pack | Bin 0 -> 609 bytes .../user2/test_commit_revert.git/packed-refs | 3 ++ .../repo_mergecommit_revert_test.go | 34 ++++++++++++++++++ 18 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 tests/gitea-repositories-meta/user2/test_commit_revert.git/HEAD create mode 100644 tests/gitea-repositories-meta/user2/test_commit_revert.git/config create mode 100644 tests/gitea-repositories-meta/user2/test_commit_revert.git/description create mode 100644 tests/gitea-repositories-meta/user2/test_commit_revert.git/info/exclude create mode 100644 tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.idx create mode 100644 tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.pack create mode 100644 tests/gitea-repositories-meta/user2/test_commit_revert.git/packed-refs create mode 100644 tests/integration/repo_mergecommit_revert_test.go diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index c22eb8c2a2..98b1f47401 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -649,3 +649,10 @@ repo_id: 49 type: 2 created_unix: 946684810 + +- + id: 98 + repo_id: 59 + type: 1 + config: "{}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 373c1caa62..f4e8376735 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1693,3 +1693,16 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + +- + id: 59 + owner_id: 2 + owner_name: user2 + lower_name: test_commit_revert + name: test_commit_revert + default_branch: main + is_empty: false + is_archived: false + is_private: true + status: 0 + num_issues: 0 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index fd51379816..79fbb981f6 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -66,7 +66,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 14 + num_repos: 15 num_teams: 0 num_members: 0 visibility: 0 diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index 8164ffb01c..e580de2fe0 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -267,7 +267,7 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re defer t.Close() var lastCommitID string - if err := t.Clone(repo.DefaultBranch); err != nil { + if err := t.Clone(repo.DefaultBranch, true); err != nil { if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { return err } diff --git a/services/repository/files/cherry_pick.go b/services/repository/files/cherry_pick.go index c1c5bfb617..5e9a0900a7 100644 --- a/services/repository/files/cherry_pick.go +++ b/services/repository/files/cherry_pick.go @@ -31,12 +31,15 @@ func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_mod log.Error("%v", err) } defer t.Close() - if err := t.Clone(opts.OldBranch); err != nil { + if err := t.Clone(opts.OldBranch, false); err != nil { return nil, err } if err := t.SetDefaultIndex(); err != nil { return nil, err } + if err := t.RefreshIndex(); err != nil { + return nil, err + } // Get the commit of the original branch commit, err := t.GetBranchCommit(opts.OldBranch) diff --git a/services/repository/files/diff.go b/services/repository/files/diff.go index 373249b114..bf8b938e21 100644 --- a/services/repository/files/diff.go +++ b/services/repository/files/diff.go @@ -21,7 +21,7 @@ func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, tr return nil, err } defer t.Close() - if err := t.Clone(branch); err != nil { + if err := t.Clone(branch, true); err != nil { return nil, err } if err := t.SetDefaultIndex(); err != nil { diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index fdf0b32f1a..5bd59251df 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -108,7 +108,7 @@ func ApplyDiffPatch(ctx context.Context, repo *repo_model.Repository, doer *user log.Error("%v", err) } defer t.Close() - if err := t.Clone(opts.OldBranch); err != nil { + if err := t.Clone(opts.OldBranch, true); err != nil { return nil, err } if err := t.SetDefaultIndex(); err != nil { diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 7f6b8137ae..4c8cfb2481 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -51,8 +51,13 @@ func (t *TemporaryUploadRepository) Close() { } // Clone the base repository to our path and set branch as the HEAD -func (t *TemporaryUploadRepository) Clone(branch string) error { - if _, _, err := git.NewCommand(t.ctx, "clone", "-s", "--bare", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath).RunStdString(nil); err != nil { +func (t *TemporaryUploadRepository) Clone(branch string, bare bool) error { + cmd := git.NewCommand(t.ctx, "clone", "-s", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath) + if bare { + cmd.AddArguments("--bare") + } + + if _, _, err := cmd.RunStdString(nil); err != nil { stderr := err.Error() if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched { return git.ErrBranchNotExist{ @@ -98,6 +103,14 @@ func (t *TemporaryUploadRepository) SetDefaultIndex() error { return nil } +// RefreshIndex looks at the current index and checks to see if merges or updates are needed by checking stat() information. +func (t *TemporaryUploadRepository) RefreshIndex() error { + if _, _, err := git.NewCommand(t.ctx, "update-index", "--refresh").RunStdString(&git.RunOpts{Dir: t.basePath}); err != nil { + return fmt.Errorf("RefreshIndex: %w", err) + } + return nil +} + // LsFiles checks if the given filename arguments are in the index func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, error) { stdOut := new(bytes.Buffer) diff --git a/services/repository/files/update.go b/services/repository/files/update.go index f01092d360..9d32876eff 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -141,7 +141,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } defer t.Close() hasOldBranch := true - if err := t.Clone(opts.OldBranch); err != nil { + if err := t.Clone(opts.OldBranch, true); err != nil { for _, file := range opts.Files { if file.Operation == "delete" { return nil, err diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index f4e1da7bb1..c893c5c43e 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -87,7 +87,7 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use defer t.Close() hasOldBranch := true - if err = t.Clone(opts.OldBranch); err != nil { + if err = t.Clone(opts.OldBranch, true); err != nil { if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { return err } diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/HEAD b/tests/gitea-repositories-meta/user2/test_commit_revert.git/HEAD new file mode 100644 index 0000000000..b870d82622 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_commit_revert.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/config b/tests/gitea-repositories-meta/user2/test_commit_revert.git/config new file mode 100644 index 0000000000..57bbcba5be --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_commit_revert.git/config @@ -0,0 +1,8 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + ignorecase = true + precomposeunicode = true +[remote "origin"] + url = https://try.gitea.io/me-heer/test_commit_revert.git diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/description b/tests/gitea-repositories-meta/user2/test_commit_revert.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_commit_revert.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/info/exclude b/tests/gitea-repositories-meta/user2/test_commit_revert.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_commit_revert.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.idx b/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.idx new file mode 100644 index 0000000000000000000000000000000000000000..77bcbe7fb4caeaf783d375068e6ffad1a61ed6f9 GIT binary patch literal 1268 zcmexg;-AdGz`(>nd%(!RKsE-sZ=f(U0|QMl3j@O_9-uH_WndU^%*Mb#HfCpFpbnPU z->08CXNQwdPL65s%82#e3p^^-eHeo7hH(5?r+vbOLGyq5_1Q-cyRFLFbfR(Hgom+n z&YyI@m=x2g^EB??V(VbJ#K@;xHu&frp3&v9&+yY;^W(8%TSdHYUR6>)@BCf-&aq!% z_g<eq9`w{<*Oqg3&xJ4l>Az#T>HAx$&y2TTFEcp*oWW#%U$m0Z)=mMznA>`cHe2?8 z-}-&)E`38kvvX?F5B_JqO#kv&o`He!90LQB0|Nu2Bm)DZKLZ2fKL!Q{9tH*`fr$z{ red+AUSviLv+z^o8#j$bj>4{T1%ou`>&%0ZEj@95$+VLx%XWjGxmLGEK literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.pack b/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.pack new file mode 100644 index 0000000000000000000000000000000000000000..7271cdaeb87741f0ed005bdae83367b7e3dd76ff GIT binary patch literal 609 zcmV-n0-pU)K|@Ob00062000M=8F-wXj!jO(FcgIMImMG$6oi_@f2my|#0A(Ou|~Et zZX+dea9<0(Jw+6WH8Y!8jJ_Ez00|qCg-o4sF<32i>@HTu_TCxmlMcSscH^qW7tBls z30|pSlt3qpg9$pcI!Z+4to2$4D3RoEUw7VjKFDaT@Ng}J>0OK|I=$&dCrz|YMwrSU zd7leSSnq*C57+QC=YnNC*Idr^ZeaL&$d4p8tx-x!A?d!osET=-24?)9P(7pUK+AC) zso?#H1Jmkt={<weFhbP)$t$|O5qFeIo~XwOV_s;OIn(5axvE}p8t{RBo#!p>nU}-1 zY5wju*HaB&@NYk--^vA4xcOc_G@|+fiMoNz>jHrdc$}NT{E=}&+{8XJCIiEX#~h>$ z3>CBufPjlDv>+w1Bvm0TGbhzhucV@c3ji-z4N{&9c$}TgK@P$o6h+ZFt6**<9}p>t zF}f5e4@**{A>YLAUI(tgJ36Dw1CYo%7E3xuq<FS87kDCj3T1*z##ocw6f}wW;-wGS za9n^PFF)L#ePCO6#bZ~G%807=tJI#HwMP3O5A;pUy|v1W{}1Axvn+LmR{Uo10VDxB zh@}E}oHH~qFf%bxNXyJgHPkDqC}G%ZemquetBCi_t4hk}oxh9UIrb|I0O*ho_BnW* zGc+<b;W9Ndw*UYLJpr$!0(hJ=G%zqTF;Pg%%t<xWE2$`9Q1@X7x*NjrW1aR17Y5D$ v>DOl;Jq!TW;|>!vc$_mdGByDK0r&vRksu6?X9r_yY{TH#5Ffe-v7O72@1GJp literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/packed-refs b/tests/gitea-repositories-meta/user2/test_commit_revert.git/packed-refs new file mode 100644 index 0000000000..1f546d7fd5 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/test_commit_revert.git/packed-refs @@ -0,0 +1,3 @@ +# pack-refs with: peeled fully-peeled sorted +46aa6ab2c881ae90e15d9ccfc947d1625c892ce5 refs/heads/develop +deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7 refs/heads/main diff --git a/tests/integration/repo_mergecommit_revert_test.go b/tests/integration/repo_mergecommit_revert_test.go new file mode 100644 index 0000000000..4d612bdcdb --- /dev/null +++ b/tests/integration/repo_mergecommit_revert_test.go @@ -0,0 +1,34 @@ +package integration + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestRepoMergeCommitRevert(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + + req := NewRequest(t, "GET", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main?ref=main&refType=branch&cherry-pick-type=revert") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + req = NewRequestWithValues(t, "POST", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main", map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "last_commit": "deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7", + "page_has_posted": "true", + "revert": "true", + "commit_summary": "reverting test commit", + "commit_message": "test message", + "commit_choice": "direct", + "new_branch_name": "test-revert-branch-1", + }) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + + // A successful revert redirects to the main branch + assert.EqualValues(t, "/user2/test_commit_revert/src/branch/main", resp.Header().Get("Location")) +}