diff --git a/pkg/gui/controllers/helpers/fixup_helper.go b/pkg/gui/controllers/helpers/fixup_helper.go index e816f425d..7177398bb 100644 --- a/pkg/gui/controllers/helpers/fixup_helper.go +++ b/pkg/gui/controllers/helpers/fixup_helper.go @@ -47,16 +47,21 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { if err != nil { return err } - if diff == "" { + + deletedLineHunks, addedLineHunks := parseDiff(diff) + + var hashes []string + warnAboutAddedLines := false + + if len(deletedLineHunks) > 0 { + hashes, err = self.blameDeletedLines(deletedLineHunks) + warnAboutAddedLines = len(addedLineHunks) > 0 + } else if len(addedLineHunks) > 0 { + hashes, err = self.blameAddedLines(addedLineHunks) + } else { return errors.New(self.c.Tr.NoChangedFiles) } - deletedLineHunks, addedLineHunks := parseDiff(diff) - if len(deletedLineHunks) == 0 { - return errors.New(self.c.Tr.NoDeletedLinesInDiff) - } - - hashes, err := self.blameDeletedLines(deletedLineHunks) if err != nil { return err } @@ -104,7 +109,7 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error { return self.c.PushContext(self.c.Contexts().LocalCommits) } - if len(addedLineHunks) > 0 { + if warnAboutAddedLines { return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.FindBaseCommitForFixup, Prompt: self.c.Tr.HunksWithOnlyAddedLinesWarning, @@ -220,6 +225,87 @@ func (self *FixupHelper) blameDeletedLines(deletedLineHunks []*hunk) ([]string, return result.ToSlice(), errg.Wait() } +func (self *FixupHelper) blameAddedLines(addedLineHunks []*hunk) ([]string, error) { + errg := errgroup.Group{} + hashesChan := make(chan []string) + + for _, h := range addedLineHunks { + errg.Go(func() error { + result := make([]string, 0, 2) + + appendBlamedLine := func(blameOutput string) { + blameLines := strings.Split(strings.TrimSuffix(blameOutput, "\n"), "\n") + if len(blameLines) == 1 { + result = append(result, strings.Split(blameLines[0], " ")[0]) + } + } + + // Blame the line before this hunk, if there is one + if h.startLineIdx > 0 { + blameOutput, err := self.c.Git().Blame.BlameLineRange(h.filename, "HEAD", h.startLineIdx, 1) + if err != nil { + return err + } + appendBlamedLine(blameOutput) + } + + // Blame the line after this hunk. We don't know how many lines the + // file has, so we can't check if there is a line after the hunk; + // let the error tell us. + blameOutput, err := self.c.Git().Blame.BlameLineRange(h.filename, "HEAD", h.startLineIdx+1, 1) + if err != nil { + // If this fails, we're probably at the end of the file (we + // could have checked this beforehand, but it's expensive). If + // there was a line before this hunk, this is fine, we'll just + // return that one; if not, the hunk encompasses the entire + // file, and we can't blame the lines before and after the hunk. + // This is an error. + if h.startLineIdx == 0 { + return errors.New("Entire file") // TODO i18n + } + } else { + appendBlamedLine(blameOutput) + } + + hashesChan <- result + return nil + }) + } + + go func() { + // We don't care about the error here, we'll check it later (in the + // return statement below). Here we only wait for all the goroutines to + // finish so that we can close the channel. + _ = errg.Wait() + close(hashesChan) + }() + + result := set.New[string]() + for hashes := range hashesChan { + if len(hashes) == 1 { + result.Add(hashes[0]) + } else if len(hashes) > 1 { + if hashes[0] == hashes[1] { + result.Add(hashes[0]) + } else { + _, index1, ok1 := self.findCommit(hashes[0]) + _, index2, ok2 := self.findCommit(hashes[1]) + if ok1 && ok2 { + result.Add(lo.Ternary(index1 < index2, hashes[0], hashes[1])) + } else if ok1 { + result.Add(hashes[0]) + } else if ok2 { + result.Add(hashes[1]) + } else { + return nil, errors.New(self.c.Tr.NoBaseCommitsFound) + } + } + } + } + + return result.ToSlice(), errg.Wait() +} + func (self *FixupHelper) findCommit(hash string) (*models.Commit, int, bool) { return lo.FindIndexOf(self.c.Model().Commits, func(commit *models.Commit) bool { return commit.Hash == hash diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index fe088f8e0..a9c8fcec5 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -42,7 +42,6 @@ type TranslationSet struct { CommitChangesWithEditor string FindBaseCommitForFixup string FindBaseCommitForFixupTooltip string - NoDeletedLinesInDiff string NoBaseCommitsFound string MultipleBaseCommitsFoundStaged string MultipleBaseCommitsFoundUnstaged string @@ -1005,7 +1004,6 @@ func EnglishTranslationSet() TranslationSet { CommitChangesWithEditor: "Commit changes using git editor", FindBaseCommitForFixup: "Find base commit for fixup", FindBaseCommitForFixupTooltip: "Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: ", - NoDeletedLinesInDiff: "No deleted lines in diff", NoBaseCommitsFound: "No base commits found", MultipleBaseCommitsFoundStaged: "Multiple base commits found. (Try staging fewer changes at once)", MultipleBaseCommitsFoundUnstaged: "Multiple base commits found. (Try staging some of the changes)", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 97a68a326..ae99829aa 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -33,7 +33,6 @@ func polishTranslationSet() TranslationSet { CommitChangesWithEditor: "Zatwierdź zmiany używając edytora git", FindBaseCommitForFixup: "Znajdź bazowy commit do poprawki", FindBaseCommitForFixupTooltip: "Znajdź commit, na którym opierają się Twoje obecne zmiany, w celu poprawienia/zmiany commita. To pozwala Ci uniknąć przeglądania commitów w Twojej gałęzi jeden po drugim, aby zobaczyć, który commit powinien być poprawiony/zmieniony. Zobacz dokumentację: ", - NoDeletedLinesInDiff: "Brak usuniętych linii w różnicach", NoBaseCommitsFound: "Nie znaleziono bazowych commitów", MultipleBaseCommitsFoundStaged: "Znaleziono wiele bazowych commitów. (Spróbuj zatwierdzić mniej zmian naraz)", MultipleBaseCommitsFoundUnstaged: "Znaleziono wiele bazowych commitów. (Spróbuj zatwierdzić część zmian)", diff --git a/pkg/integration/tests/commit/find_base_commit_for_fixup_only_added_lines.go b/pkg/integration/tests/commit/find_base_commit_for_fixup_only_added_lines.go new file mode 100644 index 000000000..281fe72f9 --- /dev/null +++ b/pkg/integration/tests/commit/find_base_commit_for_fixup_only_added_lines.go @@ -0,0 +1,84 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var FindBaseCommitForFixupOnlyAddedLines = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Finds the base commit to create a fixup for, when all staged hunks have only added lines", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.NewBranch("mybranch"). + EmptyCommit("1st commit"). + CreateFileAndAdd("file1", "line A\nline B\nline C\n"). + Commit("2nd commit"). + UpdateFileAndAdd("file1", "line A\nline B changed\nline C\n"). + Commit("3rd commit"). + CreateFileAndAdd("file2", "line X\nline Y\nline Z\n"). + Commit("4th commit"). + UpdateFile("file1", "line A\nline B changed\nline B'\nline C\n"). + UpdateFile("file2", "line W\nline X\nline Y\nline Z\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Lines( + Contains("4th commit"), + Contains("3rd commit"), + Contains("2nd commit"), + Contains("1st commit"), + ) + + // Two changes from different commits: this fails + t.Views().Files(). + Focus(). + Press(keys.Files.FindBaseCommitForFixup) + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content( + Contains("Multiple base commits found"). + Contains("3rd commit"). + Contains("4th commit"), + ). + Confirm() + + // Stage only one of the files: this succeeds + t.Views().Files(). + IsFocused(). + NavigateToLine(Contains("file1")). + PressPrimaryAction(). + Press(keys.Files.FindBaseCommitForFixup) + + t.Views().Commits(). + IsFocused(). + Lines( + Contains("4th commit"), + Contains("3rd commit").IsSelected(), + Contains("2nd commit"), + Contains("1st commit"), + ). + Press(keys.Commits.AmendToCommit) + + t.ExpectPopup().Confirmation(). + Title(Equals("Amend commit")). + Content(Contains("Are you sure you want to amend this commit with your staged files?")). + Confirm() + + // Now only the other file is modified (and unstaged); this works now + t.Views().Files(). + Focus(). + Press(keys.Files.FindBaseCommitForFixup) + + t.Views().Commits(). + IsFocused(). + Lines( + Contains("4th commit").IsSelected(), + Contains("3rd commit"), + Contains("2nd commit"), + Contains("1st commit"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index e1ded611b..d4a093de8 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -79,6 +79,7 @@ var tests = []*components.IntegrationTest{ commit.CreateTag, commit.DiscardOldFileChanges, commit.FindBaseCommitForFixup, + commit.FindBaseCommitForFixupOnlyAddedLines, commit.FindBaseCommitForFixupWarningForAddedLines, commit.Highlight, commit.History,