From f1753f36c828663951ffa2649be593ef4290ac40 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 12 Aug 2023 13:17:01 +1000 Subject: [PATCH] Add rebase from marked base commit test This also fixes a bug where after the rebase each commit in the commits view had a tick against it because we hadn't refreshed the view since the base commit was no longer marked --- README.md | 6 + .../helpers/merge_and_rebase_helper.go | 6 +- pkg/integration/components/random.go | 262 ++++++++++++++++++ pkg/integration/components/shell.go | 19 ++ .../tests/branch/rebase_from_marked_base.go | 24 +- pkg/integration/tests/demo/rebase_onto.go | 81 ++++++ pkg/integration/tests/test_list.go | 1 + 7 files changed, 385 insertions(+), 14 deletions(-) create mode 100644 pkg/integration/tests/demo/rebase_onto.go diff --git a/README.md b/README.md index 0eddae062..7ebffb36b 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,12 @@ Learn more in the [Rebase magic Youtube tutorial](https://youtu.be/4XaToVut_hs). ![custom_patch](../assets/demo/custom_patch-compressed.gif) +### Rebase from marked base commit + +Say you're on a feature branch that was itself branched off of the develop branch, and you've decided you'd rather be branching off the master branch. You need a way to rebase only the commits from your feature branch. In this demo we check to see which was the last commit on the develop branch, then press `shift+b` to mark that commit as our base commit, then press `r` on the master branch to rebase onto it, only bringing across the commits from our feature branch. Then we push our changes with `shift+p`. + +![rebase_onto](../assets/demo/rebase_onto-compressed.gif) + ### Undo You can undo the last action by pressing 'z' and redo with `ctrl+z`. Here we drop a couple of commits and then undo the actions. diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index db3ffa9ae..4c7b087aa 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -235,7 +235,7 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error { } err = self.CheckMergeOrRebase(err) if err == nil { - self.c.Modes().MarkedBaseCommit.Reset() + return self.ResetMarkedBaseCommit() } return err }) @@ -257,7 +257,9 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error { if err = self.CheckMergeOrRebase(err); err != nil { return err } - self.c.Modes().MarkedBaseCommit.Reset() + if err = self.ResetMarkedBaseCommit(); err != nil { + return err + } return self.c.PushContext(self.c.Contexts().LocalCommits) }, }, diff --git a/pkg/integration/components/random.go b/pkg/integration/components/random.go index cfd9d40ba..808089e23 100644 --- a/pkg/integration/components/random.go +++ b/pkg/integration/components/random.go @@ -210,3 +210,265 @@ var RandomFiles = []RandomFile{ {Name: `seo/alt_text2.go`, Content: `package seo`}, {Name: `moderation/comment_moderation2.go`, Content: `package moderation`}, } + +var RandomFileContents = []string{ + `package main + +import ( + "bytes" + "fmt" + "go/format" + "io/fs" + "os" + "strings" + + "github.com/samber/lo" +) + +func main() { + code := generateCode() + + formattedCode, err := format.Source(code) + if err != nil { + panic(err) + } + if err := os.WriteFile("test_list.go", formattedCode, 0o644); err != nil { + panic(err) + } +} +`, + ` +package tests + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/lazycore/pkg/utils" + "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/samber/lo" +) + +func GetTests() []*components.IntegrationTest { + // first we ensure that each test in this directory has actually been added to the above list. + testCount := 0 + + testNamesSet := set.NewFromSlice(lo.Map( + tests, + func(test *components.IntegrationTest, _ int) string { + return test.Name() + }, + )) +} +`, + ` +package components + +import ( + "os" + "strconv" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/config" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" +) + +// IntegrationTest describes an integration test that will be run against the lazygit gui. + +// our unit tests will use this description to avoid a panic caused by attempting +// to get the test's name via it's file's path. +const unitTestDescription = "test test" + +const ( + defaultWidth = 100 + defaultHeight = 100 +) +`, + `package components + +import ( + "fmt" + "time" + + "github.com/atotto/clipboard" + "github.com/jesseduffield/lazygit/pkg/config" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" +) + +type TestDriver struct { + gui integrationTypes.GuiDriver + keys config.KeybindingConfig + inputDelay int + *assertionHelper + shell *Shell +} + +func NewTestDriver(gui integrationTypes.GuiDriver, shell *Shell, keys config.KeybindingConfig, inputDelay int) *TestDriver { + return &TestDriver{ + gui: gui, + keys: keys, + inputDelay: inputDelay, + assertionHelper: &assertionHelper{gui: gui}, + shell: shell, + } +} + +// key is something like 'w' or ''. It's best not to pass a direct value, +// but instead to go through the default user config to get a more meaningful key name +func (self *TestDriver) press(keyStr string) { + self.SetCaption(fmt.Sprintf("Pressing %s", keyStr)) + self.gui.PressKey(keyStr) + self.Wait(self.inputDelay) +} +`, + `package updates + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/go-errors/errors" + + "github.com/kardianos/osext" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/constants" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// Updater checks for updates and does updates +type Updater struct { + *common.Common + Config config.AppConfigurer + OSCommand *oscommands.OSCommand +} + +// Updaterer implements the check and update methods +type Updaterer interface { + CheckForNewUpdate() + Update() +} +`, + ` +package utils + +import ( + "fmt" + "regexp" + "strings" +) + +// IsValidEmail checks if an email address is valid +func IsValidEmail(email string) bool { + // Using a regex pattern to validate email addresses + // This is a simple example and might not cover all edge cases + emailPattern := ` + "`" + `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + "`" + ` + match, _ := regexp.MatchString(emailPattern, email) + return match +} +`, + ` +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/jesseduffield/lazygit/pkg/utils" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, the current time is: %s", time.Now().Format(time.RFC3339)) + }) + + port := 8080 + utils.PrintMessage(fmt.Sprintf("Server is listening on port %d", port)) + http.ListenAndServe(fmt.Sprintf(":%d", port), nil) +} +`, + ` +package logging + +import ( + "fmt" + "os" + "time" +) + +// LogMessage represents a log message with its timestamp +type LogMessage struct { + Timestamp time.Time + Message string +} + +// Log writes a message to the log file along with a timestamp +func Log(message string) { + logFile, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Println("Error opening log file:", err) + return + } + defer logFile.Close() + + logEntry := LogMessage{ + Timestamp: time.Now(), + Message: message, + } + + logLine := fmt.Sprintf("[%s] %s\n", logEntry.Timestamp.Format("2006-01-02 15:04:05"), logEntry.Message) + _, err = logFile.WriteString(logLine) + if err != nil { + fmt.Println("Error writing to log file:", err) + } +} +`, + ` +package encryption + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" +) + +// Encrypt encrypts a plaintext using AES-GCM encryption +func Encrypt(key []byte, plaintext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + ciphertext := aesGCM.Seal(nil, nonce, plaintext, nil) + return append(nonce, ciphertext...), nil +} +`, +} diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index 4ba1bff67..4cf0990d6 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -18,6 +18,8 @@ type Shell struct { // when running the shell outside the gui we can directly panic on failure, // but inside the gui we need to close the gui before panicking fail func(string) + + randomFileContentIndex int } func NewShell(dir string, fail func(string)) *Shell { @@ -221,6 +223,16 @@ func (self *Shell) CreateNCommitsWithRandomMessages(n int) *Shell { return self } +// Creates a commit with a random file +// Only to be used in demos +func (self *Shell) RandomChangeCommit(message string) *Shell { + index := self.randomFileContentIndex + self.randomFileContentIndex++ + randomFileName := fmt.Sprintf("random-%d.go", index) + self.CreateFileAndAdd(randomFileName, RandomFileContents[index%len(RandomFileContents)]) + return self.Commit(message) +} + func (self *Shell) SetConfig(key string, value string) *Shell { self.RunCommand([]string{"git", "config", "--local", key, value}) return self @@ -361,3 +373,10 @@ func (self *Shell) Chdir(path string) *Shell { return self } + +func (self *Shell) SetAuthor(authorName string, authorEmail string) *Shell { + self.RunCommand([]string{"git", "config", "--local", "user.name", authorName}) + self.RunCommand([]string{"git", "config", "--local", "user.email", authorEmail}) + + return self +} diff --git a/pkg/integration/tests/branch/rebase_from_marked_base.go b/pkg/integration/tests/branch/rebase_from_marked_base.go index fb6ede722..c2ee307e3 100644 --- a/pkg/integration/tests/branch/rebase_from_marked_base.go +++ b/pkg/integration/tests/branch/rebase_from_marked_base.go @@ -40,12 +40,12 @@ var RebaseFromMarkedBase = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("active one")). Press(keys.Commits.MarkCommitAsBaseForRebase). Lines( - Contains("active three"), - Contains("active two"), + Contains("active three").Contains("✓"), + Contains("active two").Contains("✓"), Contains("↑↑↑ Will rebase from here ↑↑↑ active one"), - Contains("three"), - Contains("two"), - Contains("one"), + Contains("three").DoesNotContain("✓"), + Contains("two").DoesNotContain("✓"), + Contains("one").DoesNotContain("✓"), ) t.Views().Information().Content(Contains("Marked a base commit for rebase")) @@ -66,13 +66,13 @@ var RebaseFromMarkedBase = NewIntegrationTest(NewIntegrationTestArgs{ Confirm() t.Views().Commits().Lines( - Contains("active three"), - Contains("active two"), - Contains("target two"), - Contains("target one"), - Contains("three"), - Contains("two"), - Contains("one"), + Contains("active three").DoesNotContain("✓"), + Contains("active two").DoesNotContain("✓"), + Contains("target two").DoesNotContain("✓"), + Contains("target one").DoesNotContain("✓"), + Contains("three").DoesNotContain("✓"), + Contains("two").DoesNotContain("✓"), + Contains("one").DoesNotContain("✓"), ) }, }) diff --git a/pkg/integration/tests/demo/rebase_onto.go b/pkg/integration/tests/demo/rebase_onto.go new file mode 100644 index 000000000..381a2f050 --- /dev/null +++ b/pkg/integration/tests/demo/rebase_onto.go @@ -0,0 +1,81 @@ +package demo + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var RebaseOnto = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Rebase with '--onto' flag. We start with a feature branch on the develop branch that we want to rebase onto the master branch", + ExtraCmdArgs: []string{}, + Skip: false, + IsDemo: true, + SetupConfig: func(config *config.AppConfig) { + config.UserConfig.Gui.NerdFontsVersion = "3" + }, + SetupRepo: func(shell *Shell) { + shell.CreateNCommitsWithRandomMessages(60) + shell.NewBranch("develop") + + shell.SetAuthor("Joe Blow", "joeblow@gmail.com") + + shell.RandomChangeCommit("Develop commit 1") + shell.RandomChangeCommit("Develop commit 2") + shell.RandomChangeCommit("Develop commit 3") + + shell.SetAuthor("Jesse Duffield", "jesseduffield@gmail.com") + + shell.NewBranch("feature/demo") + + shell.RandomChangeCommit("Feature commit 1") + shell.RandomChangeCommit("Feature commit 2") + shell.RandomChangeCommit("Feature commit 3") + + shell.CloneIntoRemote("origin") + + shell.SetBranchUpstream("feature/demo", "origin/feature/demo") + shell.SetBranchUpstream("develop", "origin/develop") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.SetCaptionPrefix("Rebase from marked base commit") + t.Wait(1000) + + // first we focus the commits view, then expand to show the branches against each commit + // Then we go back to normal value, mark the last develop branch commit as the marked commit + // Then go to the branches view and press 'r' on the master branch to rebase onto it + // then we force push our changes. + + t.Views().Commits(). + Focus(). + Press(keys.Universal.PrevScreenMode). + Wait(500). + NavigateToLine(Contains("Develop commit 3")). + Wait(500). + Press(keys.Commits.MarkCommitAsBaseForRebase). + Wait(1000). + Press(keys.Universal.NextScreenMode). + Wait(500) + + t.Views().Branches(). + Focus(). + Wait(500). + NavigateToLine(Contains("master")). + Wait(500). + Press(keys.Branches.RebaseBranch). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Contains("Rebase 'feature/demo' from marked base onto 'master'")). + Select(Contains("Simple rebase")). + Confirm() + }). + Wait(1000). + Press(keys.Universal.Push). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Contains("Force push")). + Content(AnyString()). + Wait(500). + Confirm() + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 8c9527bec..a63481be0 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -98,6 +98,7 @@ var tests = []*components.IntegrationTest{ demo.Filter, demo.InteractiveRebase, demo.NukeWorkingTree, + demo.RebaseOnto, demo.StageLines, demo.Undo, demo.WorktreeCreateFromBranches,