1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-04 10:34:55 +02:00

Fix "move patch into new commit" for partial hunk (#2507)

This commit is contained in:
Stefan Haller 2023-03-18 08:17:47 +01:00 committed by GitHub
parent 81ea3107ed
commit 4b4dccfd7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 534 additions and 44 deletions

View File

@ -45,9 +45,7 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
// apply each patch in reverse
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
_ = self.rebase.AbortRebase()
return err
}
@ -73,9 +71,8 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
// apply each patch forward
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
// Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them
return err
}
@ -121,9 +118,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
// apply each patch in reverse
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
_ = self.rebase.AbortRebase()
return err
}
@ -132,6 +127,12 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
return err
}
patch, err := self.diffHeadAgainstCommit(commits[sourceCommitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
@ -139,10 +140,9 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
self.rebase.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
// Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them
return err
}
@ -175,9 +175,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
if err := self.PatchManager.ApplyPatches(true); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
_ = self.rebase.AbortRebase()
}
return err
}
@ -187,17 +185,21 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
return err
}
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if self.rebase.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
self.rebase.onSuccessfulContinue = func() error {
// add patches to index
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
_ = self.rebase.AbortRebase()
}
return err
}
@ -221,9 +223,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
}
if err := self.PatchManager.ApplyPatches(true); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
_ = self.rebase.AbortRebase()
return err
}
@ -232,18 +232,20 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err
}
// add patches to index
if err := self.PatchManager.ApplyPatches(false); err != nil {
if err := self.rebase.AbortRebase(); err != nil {
return err
}
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
if err != nil {
_ = self.rebase.AbortRebase()
return err
}
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
_ = self.rebase.AbortRebase()
return err
}
head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
err := self.commit.CommitCmdObj(new_message).Run()
if err != nil {
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
return err
}
@ -254,3 +256,12 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
self.PatchManager.Reset()
return self.rebase.ContinueRebase()
}
// We have just applied a patch in reverse to discard it from a commit; if we
// now try to apply the patch again to move it to a later commit, or to the
// index, then this would conflict "with itself" in case the patch contained
// only some lines of a range of adjacent added lines. To solve this, we
// get the diff of HEAD and the original commit and then apply that.
func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string, error) {
return self.cmd.New(fmt.Sprintf("git diff HEAD..%s", commit.Sha)).RunWithOutput()
}

View File

@ -137,7 +137,7 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseShaOrRoot string
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --no-autosquash %s", baseShaOrRoot)
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash %s", baseShaOrRoot)
self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr)

View File

@ -26,7 +26,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
testName: "successful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash master`, "", nil),
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
@ -35,7 +35,7 @@ func TestRebaseRebaseBranch(t *testing.T) {
testName: "unsuccessful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash master`, "", errors.New("error")),
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash master`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
@ -125,7 +125,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
commitIndex: 0,
fileName: "test999.txt",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash abcdef`, "", nil).
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash abcdef`, "", nil).
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).

View File

@ -286,3 +286,13 @@ func (p *PatchManager) IsEmpty() bool {
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
return from != p.From || to != p.To || reverse != p.reverse
}
func (p *PatchManager) AllFilesInPatch() []string {
files := make([]string, 0, len(p.fileInfoMap))
for filename := range p.fileInfoMap {
files = append(files, filename)
}
return files
}

View File

@ -94,6 +94,16 @@ func (self *Shell) CreateFile(path string, content string) *Shell {
return self
}
func (self *Shell) DeleteFile(path string) *Shell {
fullPath := filepath.Join(self.dir, path)
err := os.Remove(fullPath)
if err != nil {
self.fail(fmt.Sprintf("error deleting file: %s\n%s", fullPath, err))
}
return self
}
func (self *Shell) CreateDir(path string) *Shell {
fullPath := filepath.Join(self.dir, path)
if err := os.MkdirAll(fullPath, 0o755); err != nil {
@ -171,6 +181,13 @@ func (self *Shell) UpdateFileAndAdd(fileName string, fileContents string) *Shell
GitAdd(fileName)
}
// convenience method for deleting a file and adding it
func (self *Shell) DeleteFileAndAdd(fileName string) *Shell {
return self.
DeleteFile(fileName).
GitAdd(fileName)
}
// creates commits 01, 02, 03, ..., n with a new file in each
// The reason for padding with zeroes is so that it's easier to do string
// matches on the commit messages when there are many of them

View File

@ -0,0 +1,88 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToEarlierCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to an earlier commit",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir")
shell.CreateFileAndAdd("dir/file1", "file1 content")
shell.CreateFileAndAdd("dir/file2", "file2 content")
shell.Commit("first commit")
shell.CreateFileAndAdd("unrelated-file", "")
shell.Commit("destination commit")
shell.UpdateFileAndAdd("dir/file1", "file1 content with old changes")
shell.DeleteFileAndAdd("dir/file2")
shell.CreateFileAndAdd("dir/file3", "file3 content")
shell.Commit("commit to move from")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit to move from").IsSelected(),
Contains("destination commit"),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("dir").IsSelected(),
Contains(" M file1"),
Contains(" D file2"),
Contains(" A file3"),
).
PressPrimaryAction().
PressEscape()
t.Views().Information().Content(Contains("building patch"))
t.Views().Commits().
IsFocused().
SelectNextItem()
t.Common().SelectPatchOption(Contains("move patch to selected commit"))
t.Views().Commits().
IsFocused().
Lines(
Contains("commit to move from"),
Contains("destination commit").IsSelected(),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("dir").IsSelected(),
Contains(" M file1"),
Contains(" D file2"),
Contains(" A file3"),
Contains("A unrelated-file"),
).
PressEscape()
t.Views().Commits().
IsFocused().
SelectPreviousItem().
PressEnter()
// the original commit has no more files in it
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("(none)"),
)
},
})

View File

@ -0,0 +1,70 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToIndexPartOfAdjacentAddedLines = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to the index, with only some lines of a range of adjacent added lines in the patch",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "1st line\n2nd line\n")
shell.Commit("commit to move from")
shell.UpdateFileAndAdd("unrelated-file", "")
shell.Commit("third commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("third commit").IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressEnter()
t.Views().PatchBuilding().
IsFocused().
PressEnter().
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Common().SelectPatchOption(Contains("move patch out into index"))
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
Tap(func() {
t.Views().Main().
Content(Contains("+2nd line").
DoesNotContain("1st line"))
})
t.Views().Files().
Focus().
ContainsLines(
Contains("M").Contains("file1"),
)
t.Views().Main().
Content(Contains("+1st line\n 2nd line\n"))
},
})

View File

@ -0,0 +1,89 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToLaterCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to a later commit",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir")
shell.CreateFileAndAdd("dir/file1", "file1 content")
shell.CreateFileAndAdd("dir/file2", "file2 content")
shell.Commit("first commit")
shell.UpdateFileAndAdd("dir/file1", "file1 content with old changes")
shell.DeleteFileAndAdd("dir/file2")
shell.CreateFileAndAdd("dir/file3", "file3 content")
shell.Commit("commit to move from")
shell.CreateFileAndAdd("unrelated-file", "")
shell.Commit("destination commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("destination commit").IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("dir").IsSelected(),
Contains(" M file1"),
Contains(" D file2"),
Contains(" A file3"),
).
PressPrimaryAction().
PressEscape()
t.Views().Information().Content(Contains("building patch"))
t.Views().Commits().
IsFocused().
SelectPreviousItem()
t.Common().SelectPatchOption(Contains("move patch to selected commit"))
t.Views().Commits().
IsFocused().
Lines(
Contains("destination commit").IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("dir").IsSelected(),
Contains(" M file1"),
Contains(" D file2"),
Contains(" A file3"),
Contains("A unrelated-file"),
).
PressEscape()
t.Views().Commits().
IsFocused().
SelectNextItem().
PressEnter()
// the original commit has no more files in it
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("(none)"),
)
},
})

View File

@ -0,0 +1,96 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToLaterCommitPartialHunk = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to a later commit, with only parts of a hunk in the patch",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "1st line\n2nd line\n")
shell.Commit("commit to move from")
shell.UpdateFileAndAdd("unrelated-file", "")
shell.Commit("destination commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("destination commit").IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressEnter()
t.Views().PatchBuilding().
IsFocused().
PressEnter().
PressPrimaryAction().
PressEscape()
t.Views().Information().Content(Contains("building patch"))
t.Views().CommitFiles().
IsFocused().
PressEscape()
t.Views().Commits().
IsFocused().
SelectPreviousItem()
t.Common().SelectPatchOption(Contains("move patch to selected commit"))
t.Views().Commits().
IsFocused().
Lines(
Contains("destination commit").IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
Contains("unrelated-file"),
).
Tap(func() {
t.Views().Main().
Content(Contains("+1st line\n 2nd line"))
}).
PressEscape()
t.Views().Commits().
IsFocused().
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
Tap(func() {
t.Views().Main().
Content(Contains("+2nd line").
DoesNotContain("1st line"))
})
},
})

View File

@ -11,13 +11,17 @@ var MoveToNewCommit = NewIntegrationTest(NewIntegrationTestArgs{
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "file1 content")
shell.CreateDir("dir")
shell.CreateFileAndAdd("dir/file1", "file1 content")
shell.CreateFileAndAdd("dir/file2", "file2 content")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "file1 content with old changes")
shell.Commit("second commit")
shell.UpdateFileAndAdd("dir/file1", "file1 content with old changes")
shell.DeleteFileAndAdd("dir/file2")
shell.CreateFileAndAdd("dir/file3", "file3 content")
shell.Commit("commit to move from")
shell.UpdateFileAndAdd("file1", "file1 content with new changes")
shell.UpdateFileAndAdd("dir/file1", "file1 content with new changes")
shell.Commit("third commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
@ -25,7 +29,7 @@ var MoveToNewCommit = NewIntegrationTest(NewIntegrationTestArgs{
Focus().
Lines(
Contains("third commit").IsSelected(),
Contains("second commit"),
Contains("commit to move from"),
Contains("first commit"),
).
SelectNextItem().
@ -34,18 +38,35 @@ var MoveToNewCommit = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
Contains("dir").IsSelected(),
Contains(" M file1"),
Contains(" D file2"),
Contains(" A file3"),
).
PressPrimaryAction()
PressPrimaryAction().
PressEscape()
t.Views().Information().Content(Contains("building patch"))
t.Common().SelectPatchOption(Contains("move patch into new commit"))
t.Views().Commits().
IsFocused().
Lines(
Contains("third commit"),
Contains(`Split from "commit to move from"`).IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
Contains("dir").IsSelected(),
Contains(" M file1"),
Contains(" D file2"),
Contains(" A file3"),
).
PressEscape()
@ -53,8 +74,8 @@ var MoveToNewCommit = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Lines(
Contains("third commit"),
Contains(`Split from "second commit"`).IsSelected(),
Contains("second commit"),
Contains(`Split from "commit to move from"`).IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
SelectNextItem().

View File

@ -0,0 +1,83 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveToNewCommitPartialHunk = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a patch from a commit to a new commit, with only parts of a hunk in the patch",
ExtraCmdArgs: "",
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file1", "")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file1", "1st line\n2nd line\n")
shell.Commit("commit to move from")
shell.UpdateFileAndAdd("file1", "1st line\n2nd line\n3rd line\n")
shell.Commit("third commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("third commit").IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
PressEnter()
t.Views().PatchBuilding().
IsFocused().
PressEnter().
PressPrimaryAction()
t.Views().Information().Content(Contains("building patch"))
t.Common().SelectPatchOption(Contains("move patch into new commit"))
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
Tap(func() {
t.Views().Main().
Content(Contains("+1st line\n 2nd line"))
}).
PressEscape()
t.Views().Commits().
IsFocused().
Lines(
Contains("third commit"),
Contains(`Split from "commit to move from"`).IsSelected(),
Contains("commit to move from"),
Contains("first commit"),
).
SelectNextItem().
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("file1").IsSelected(),
).
Tap(func() {
t.Views().Main().
Content(Contains("+2nd line").
DoesNotContain("1st line"))
})
},
})

View File

@ -101,10 +101,15 @@ var tests = []*components.IntegrationTest{
patch_building.ApplyInReverse,
patch_building.ApplyInReverseWithConflict,
patch_building.CopyPatchToClipboard,
patch_building.MoveToEarlierCommit,
patch_building.MoveToIndex,
patch_building.MoveToIndexPartOfAdjacentAddedLines,
patch_building.MoveToIndexPartial,
patch_building.MoveToIndexWithConflict,
patch_building.MoveToLaterCommit,
patch_building.MoveToLaterCommitPartialHunk,
patch_building.MoveToNewCommit,
patch_building.MoveToNewCommitPartialHunk,
patch_building.RemoveFromCommit,
patch_building.ResetWithEscape,
patch_building.SelectAllFiles,