Compare commits

...
69 Commits
Author SHA1 Message Date
Jesse Duffield d9548b5d00 start breaking up git struct 2022-01-02 10:34:33 +11:00
Jesse Duffield e3da89efb7 do dependency injection up front and in one place 2022-01-02 10:21:32 +11:00
Jesse Duffield 19d2994af8 no more mocking command 2021-12-31 10:46:34 +11:00
Jesse Duffield ad4fd67db3 WIP 2021-12-31 10:44:47 +11:00
Jesse Duffield 5f36eb507a more test refactoring 2021-12-31 10:24:53 +11:00
Jesse Duffield 1caec9114c refactor sync test 2021-12-31 10:04:32 +11:00
Jesse Duffield e3af0ed43a refactor files_test.go 2021-12-31 10:04:25 +11:00
Jesse Duffield 18c39c5f24 stash and tags loaders 2021-12-30 17:48:47 +11:00
Jesse Duffield 07afb5359e move remotes loader into loaders package 2021-12-30 17:36:21 +11:00
Jesse Duffield 3f1cda88ed move reflog commit loader into loaders package 2021-12-30 17:25:36 +11:00
Jesse Duffield edd43bcbeb WIP 2021-12-30 17:19:01 +11:00
Jesse Duffield 061db91002 WIP 2021-12-30 13:44:41 +11:00
Jesse Duffield 2455faec1b more refactoring 2021-12-30 13:35:10 +11:00
Jesse Duffield 6fddf2aa8b WIP 2021-12-30 13:13:25 +11:00
Jesse Duffield c24bb11141 updating specs 2021-12-30 13:11:58 +11:00
Jesse Duffield 0eea75e8c6 better typing for rebase mode 2021-12-30 12:11:57 +11:00
Jesse Duffield b21997d6b4 fix logging 2021-12-30 11:35:15 +11:00
Jesse Duffield e94e8fc5b6 refactor 2021-12-30 11:22:29 +11:00
Jesse Duffield d8084cd558 WIP 2021-12-30 10:44:08 +11:00
Jesse Duffield 65f910ebd8 WIP 2021-12-30 10:41:15 +11:00
Jesse Duffield 26c07b1ab3 WIP 2021-12-29 15:05:52 +11:00
Jesse Duffield 3d4470a6c0 WIP 2021-12-29 14:33:38 +11:00
Jesse Duffield 053a66a7be refactoring the config struct 2021-12-29 12:03:35 +11:00
Jesse Duffield 985fe482e8 align Gui struct with GitCommand 2021-12-29 11:50:20 +11:00
Jesse Duffield 4ab4af441e no more config in git command struct 2021-12-29 11:41:33 +11:00
Jesse Duffield bdc54a5deb introduce Common struct for passing around common stuff 2021-12-29 11:37:15 +11:00
Jesse Duffield d913c04109 WIP 2021-12-29 09:14:39 +11:00
Jesse Duffield 9ef65574db refactor to rename pull_request to hosting_service and apply SRP 2021-12-29 09:01:06 +11:00
Jesse Duffield f89747451a allow opening a commit in the browser 2021-12-29 09:01:06 +11:00
Francisco Miamoto 8a76b5a4ee use custom handler only for branch commits context 2021-12-28 15:54:19 +11:00
Francisco Miamoto 1a7d0cd7ae add binding to go to bottom on lists
Related to #1584
2021-12-28 15:54:19 +11:00
Francisco Miamoto 2696a63a0a fix pushTag test by adding extra enter 2021-12-26 17:08:31 +11:00
Francisco Miamoto 8c8b925b3a set tag index directly
We can do this since they are already sorted by date created.
2021-12-26 17:08:31 +11:00
Francisco Miamoto b8735cc609 fix tags integration tests 2021-12-26 17:08:31 +11:00
Francisco Miamoto 517dab7d05 add annotated tag integration test 2021-12-26 17:08:31 +11:00
Francisco Miamoto e5f0301c66 update docs on integration tests
The instructions provided were not working as expected.
2021-12-26 17:08:31 +11:00
Francisco Miamoto eff6c4283b change formatting for menu items 2021-12-26 17:08:31 +11:00
Francisco Miamoto 7888ff6cb9 set show cancel as true for tag menu 2021-12-26 17:08:31 +11:00
Francisco Miamoto e7a005f44d update translations to match conventions 2021-12-26 17:08:31 +11:00
Francisco Miamoto 3e58797096 show tag menu for creation on tags tab 2021-12-26 17:08:31 +11:00
Francisco Miamoto b1d6ccddfb support creating annotated tags 2021-12-26 17:08:31 +11:00
Jesse Duffield 4df003cc44 handle ssh protocol 2021-12-26 17:05:05 +11:00
Jesse Duffield d9db5ccfbe refactor to use regex for matching git service URL 2021-12-26 16:48:23 +11:00
Jesse Duffield 7bc8b96aba add FindNamedMatches function in utils 2021-12-26 16:48:23 +11:00
Marius Bergmann 38743ec99f Suggest existing remote for non-tracking branch
Currently, when pushing or pulling a branch that has no tracking remote,
lazygit suggests the (hard-coded) remote named 'origin'. However, a
repository might not have a remote with this name, in which case the
suggestion makes no sense. This happens to me quite regularly when I
choose a more meaningful name than 'origin' for a remote.

This change keeps the current behavior by suggesting 'origin' when there
is either a remote with that name or no remote at all. However, when
'origin' does not exist, the name of the first remote is suggested.

Suggest existing remote for non-tracking branch

Currently, when pushing or pulling a branch that has no tracking remote,
lazygit suggests the (hard-coded) remote named 'origin'. However, a
repository might not have a remote with this name, in which case the
suggestion makes no sense. This happens to me quite regularly when I
choose a more meaningful name than 'origin' for a remote.

This change keeps the current behavior by suggesting 'origin' when there
is either a remote with that name or no remote at all. However, when
'origin' does not exist, the name of the first existing remote is
suggested.
2021-12-26 15:47:58 +11:00
justinsb 630de34bf2 Use "reword" for amending a commit message everywhere
We were inconsistent about "rename" vs "reword" for commits.  reword
is the term used in git itself (for example, in rebase).
2021-12-25 22:34:41 +11:00
Tsvetomir Bonev fca2f90f57 chore: add funtoo linux instructions 2021-12-25 22:26:46 +11:00
Mark Sagi-Kazar fdf0d4a2c3 implement signoff
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-12-25 12:01:55 +11:00
Mark Sagi-Kazar b4ea565c99 add signoff config
Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
2021-12-25 12:01:55 +11:00
Cokile 76e6745526 fix typo 2021-12-25 11:54:27 +11:00
Cokile 3771f9c98b support config unified color for commit authors 2021-12-25 11:54:27 +11:00
Chris Burgin 4f627e5d27 wrap branch names in quotes 2021-12-25 11:50:06 +11:00
Erik Scrafford 089e3bf4fe change branch regex to only grab the first remote on each line of branch command 2021-12-23 19:30:47 +11:00
Mark Kopenga d5c1cfda4b Merge pull request #1624 from sirodoht/fix-filetree-docs
Fix `showFileTree` default value in docs
2021-12-13 14:34:30 +01:00
Theodore Keloglou 3926d0f278 fix show filetree default value in docs 2021-12-13 13:28:14 +00:00
Jesse Duffield 18283ad41b add popup handler for easier testing 2021-12-06 22:37:28 +11:00
Jesse Duffield 1996eddd91 more efficient context diff size changing 2021-12-06 22:37:28 +11:00
DerTeta de0e885c65 Refresh staging- or patch building panel when rendering their respective context 2021-12-06 22:37:28 +11:00
DerTeta f7ffbbd72a Add a menu entry and keybinding to { for decreasing the context size 2021-12-06 22:37:28 +11:00
DerTeta 0fbde05928 Add a menu item and keybinding to } to increase the context size 2021-12-06 22:37:28 +11:00
DerTeta ba844c18a5 Add the DecreaseContextInDiffView function 2021-12-06 22:37:28 +11:00
DerTeta e1cf6912db Add the IncreaseContextInDiffView function 2021-12-06 22:37:28 +11:00
DerTeta c99d373e13 Use DiffContextSize in ShowStashEntryCmdStr 2021-12-06 22:37:28 +11:00
DerTeta ecfafb6fbe Use DiffContextSize in ShowCmdStr 2021-12-06 22:37:28 +11:00
DerTeta 14d9e776be Use DiffContextSize in ShowFileDiffStr 2021-12-06 22:37:28 +11:00
DerTeta ca88620e8f Use DiffContextSize in WorkTreeFileDiffCmdStr 2021-12-06 22:37:28 +11:00
DerTeta 9feaf5d70f Add the DiffContextSize setting to GitConfig
It defaults to 3 lines, which is the default value for git.
2021-12-06 22:37:28 +11:00
DerTeta 3e3151f86a Fix: Don't access a view if it's nil
The way the `if` expression in `deactivateContext` was composed,
it was possible to have it to evaluate to `true` even though the
`view` variable was `nil`.

As far as I can tell, this seems to be only possible during tests.
Nonetheless, I think the expression looks more "correct" this way.
2021-12-06 22:37:28 +11:00
alextrastero 28cdcddb0a Update README.md
add a link to ubuntu installation instructions
2021-12-01 18:00:50 +11:00
199 changed files with 4318 additions and 3592 deletions
+11 -1
View File
@@ -21,11 +21,13 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
- [Binary releases](#binary-releases)
- [Homebrew](#homebrew)
- [MacPorts](#macports)
- [Ubuntu](#ubuntu)
- [Void Linux](#void-linux)
- [Scoop (Windows)](#scoop-windows)
- [Arch Linux](#arch-linux)
- [Fedora and CentOS 7](#fedora-and-centos-7)
- [Solus Linux](#solus-linux)
- [Funtoo Linux](#funtoo-linux)
- [FreeBSD](#freebsd)
- [Conda](#conda)
- [Go](#go)
@@ -141,6 +143,14 @@ sudo dnf install lazygit
sudo eopkg install lazygit
```
### Funtoo Linux
Funtoo Linux has an autogenerated lazygit package in [dev-kit](https://github.com/funtoo/dev-kit/tree/1.4-release/dev-vcs/lazygit):
```sh
sudo emerge dev-vcs/lazygit
```
### FreeBSD
```sh
@@ -256,7 +266,7 @@ See the [docs](docs/Custom_Command_Keybindings.md)
- Easily check out recent branches
- Scroll through logs/diffs of branches/commits/stash
- Quick pushing/pulling
- Squash down and rename commits
- Squash down and reword commits
### Resolving merge conflicts
+28 -3
View File
@@ -45,17 +45,17 @@ gui:
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
showFileTree: false # for rendering changes files in a tree format
showFileTree: true # for rendering changes files in a tree format
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
showCommandLog: true
commandLogSize: 8
authorColors: # in case you're not happy with the randomly assigned colour
'John Smith': '#ff0000'
git:
paging:
colorArg: always
useConfig: false
commit:
signOff: false
merging:
# only applicable to unix users
manualCommit: false
@@ -76,6 +76,7 @@ git:
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
parseEmoji: false
diffContextSize: 3 # how many lines of context are shown around a change in diffs
os:
editCommand: '' # see 'Configuring File Editing' section
editCommandTemplate: '{{editor}} {{filename}}'
@@ -153,6 +154,8 @@ keybinding:
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
increaseContextInDiffView: '}'
decreaseContextInDiffView: '{'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -373,6 +376,28 @@ Alternatively you may have bold fonts disabled in your terminal, in which case e
If you're still having trouble please raise an issue.
## Custom Author Color
Lazygit will assign a random color for every commit author in the commits pane by default.
You can customize the color in case you're not happy with the randomly assigned one:
```yaml
gui:
authorColors:
'John Smith': '#ff0000' # use red for John Smith
```
You can use wildcard to set a unified color in case your are lazy to customize the color for every author or you just want a single color for all/other authors:
```yaml
gui:
authorColors:
# use red for John Smith
'John Smith': '#ff0000'
# use blue for other authors
'*': '#0000ff'
```
## Example Coloring
![border example](../../assets/colored-border-example.png)
+9 -7
View File
@@ -33,29 +33,31 @@ git commit -am "myfile1"
## Running tests
To run all tests
To run all tests - assuming you're at the project root:
```
go test pkg/gui/gui_test.go
go test ./pkg/gui/
```
To run them in parallel
```
PARALLEL=true go test pkg/gui/gui_test.go
PARALLEL=true go test ./pkg/gui
```
To run a single test
```
go test pkg/gui/gui_test.go -run /<test name>
go test ./pkg/gui -run /<test name>
# For example, to run the `tags` test:
go test ./pkg/gui -run /tags
```
To run a test at a certain speed
```
SPEED=2 go test pkg/gui/gui_test.go -run /<test name>
SPEED=2 go test ./pkg/gui -run /<test name>
```
To update a snapshot
```
UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /<test name>
UPDATE_SNAPSHOTS=true go test ./pkg/gui -run /<test name>
```
## Creating a new test
@@ -66,7 +68,7 @@ To create a new test:
3) If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used.
4) From the lazygit root directory, run:
```
RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /<test name>
RECORD_EVENTS=true go test ./pkg/gui -run /<test name>
```
5) Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better!
6) Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created.
+1 -1
View File
@@ -121,7 +121,7 @@
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>R</kbd>: reword commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>F</kbd>: create fixup commit for this commit
+20 -15
View File
@@ -17,6 +17,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
@@ -27,14 +28,12 @@ import (
// App struct
type App struct {
closers []io.Closer
*common.Common
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *oscommands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.TranslationSet
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
@@ -97,27 +96,35 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
userConfig := config.GetUserConfig()
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.GetUserConfig().Gui.Language)
log := newLogger(config)
tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language)
if err != nil {
return app, err
}
app.Common = &common.Common{
Log: log,
Tr: tr,
UserConfig: userConfig,
Debug: config.GetDebug(),
}
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
}
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
app.OSCommand = oscommands.NewOSCommand(app.Common)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand)
if err != nil {
return app, err
}
@@ -128,17 +135,15 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
}
app.GitCommand, err = commands.NewGitCommand(
app.Log,
app.Common,
app.OSCommand,
app.Tr,
app.Config,
git_config.NewStdCachedGitConfig(app.Log),
)
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
app.Gui, err = gui.NewGui(app.Common, app.GitCommand, app.OSCommand, config, app.Updater, filterPath, showRecentRepos)
if err != nil {
return app, err
}
@@ -146,7 +151,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
if err != nil {
@@ -203,7 +208,7 @@ func (app *App) setupRepo() (bool, error) {
}
shouldInitRepo := true
notARepository := app.Config.GetUserConfig().NotARepository
notARepository := app.UserConfig.NotARepository
if notARepository == "prompt" {
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
@@ -231,7 +236,7 @@ func (app *App) setupRepo() (bool, error) {
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
if err := app.OSCommand.Cmd.New("git init").Run(); err != nil {
return false, err
}
}
+36 -24
View File
@@ -11,19 +11,19 @@ import (
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, base string) error {
return c.RunCommand("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))
return c.Cmd.New(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))).Run()
}
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (c *GitCommand) CurrentBranchName() (string, string, error) {
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
branchName, err := c.Cmd.New("git symbolic-ref --short HEAD").RunWithOutput()
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
output, err := c.RunCommandWithOutput("git branch --contains")
output, err := c.Cmd.New("git branch --contains").RunWithOutput()
if err != nil {
return "", "", err
}
@@ -47,7 +47,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command = "git branch -D"
}
return c.OSCommand.RunCommand("%s %s", command, c.OSCommand.Quote(branch))
return c.Cmd.New(fmt.Sprintf("%s %s", command, c.OSCommand.Quote(branch))).Run()
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
@@ -61,36 +61,41 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
if options.Force {
forceArg = " --force"
}
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch)), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
return c.Cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch))).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
AddEnvVars(options.EnvVars...).
Run()
}
// GetBranchGraph gets the color-formatted graph of the log for the given branch
// Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
cmdStr := c.GetBranchGraphCmdStr(branchName)
return c.OSCommand.RunCommandWithOutput(cmdStr)
return c.GetBranchGraphCmdObj(branchName).RunWithOutput()
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))
output, err := c.Cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))).RunWithOutput()
return strings.TrimSpace(output), err
}
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
func (c *GitCommand) GetBranchGraphCmdObj(branchName string) oscommands.ICmdObj {
branchLogCmdTemplate := c.UserConfig.Git.BranchLogCmd
templateValues := map[string]string{
"branchName": c.OSCommand.Quote(branchName),
}
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
return c.Cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues))
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.RunCommand("git branch -u %s", c.OSCommand.Quote(upstream))
return c.Cmd.New("git branch -u " + c.OSCommand.Quote(upstream)).Run()
}
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
return c.Cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))).Run()
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
@@ -105,11 +110,11 @@ func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string
// current branch
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.OSCommand.RunCommandWithOutput(command, to, from)
pushableCount, err := c.Cmd.New(fmt.Sprintf(command, to, from)).RunWithOutput()
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to)
pullableCount, err := c.Cmd.New(fmt.Sprintf(command, from, to)).RunWithOutput()
if err != nil {
return "?", "?"
}
@@ -122,40 +127,47 @@ type MergeOpts struct {
// Merge merge
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
mergeArg := ""
if c.UserConfig.Git.Merging.Args != "" {
mergeArg = " " + c.UserConfig.Git.Merging.Args
}
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, c.OSCommand.Quote(branchName))
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, c.OSCommand.Quote(branchName))
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return c.OSCommand.RunCommand(command)
return c.OSCommand.Cmd.New(command).Run()
}
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.RunCommand("git merge --abort")
return c.Cmd.New("git merge --abort").Run()
}
func (c *GitCommand) IsHeadDetached() bool {
err := c.RunCommand("git symbolic-ref -q HEAD")
err := c.Cmd.New("git symbolic-ref -q HEAD").Run()
return err != nil
}
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.RunCommand("git reset --hard " + c.OSCommand.Quote(ref))
return c.Cmd.New("git reset --hard " + c.OSCommand.Quote(ref)).Run()
}
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.RunCommand("git reset --soft " + c.OSCommand.Quote(ref))
return c.Cmd.New("git reset --soft " + c.OSCommand.Quote(ref)).Run()
}
func (c *GitCommand) ResetMixed(ref string) error {
return c.RunCommand("git reset --mixed " + c.OSCommand.Quote(ref))
return c.Cmd.New("git reset --mixed " + c.OSCommand.Quote(ref)).Run()
}
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.RunCommand("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))
return c.Cmd.New(fmt.Sprintf("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))).Run()
}
func (c *GitCommand) GetRawBranches() (string, error) {
return c.Cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).RunWithOutput()
}
+69 -157
View File
@@ -1,120 +1,85 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetCommitDifferences is a function.
func TestGitCommandGetCommitDifferences(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, string)
testName string
runner *oscommands.FakeCmdObjRunner
expectedPushables string
expectedPullables string
}
scenarios := []scenario{
{
"Can't retrieve pushable count",
func(string, ...string) *exec.Cmd {
return secureexec.Command("test")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
assert.EqualValues(t, "?", pullableCount)
},
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "", errors.New("error")),
"?", "?",
},
{
"Can't retrieve pullable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return secureexec.Command("test")
}
return secureexec.Command("echo")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
assert.EqualValues(t, "?", pullableCount)
},
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
Expect("git rev-list HEAD..@{u} --count", "", errors.New("error")),
"?", "?",
},
{
"Retrieve pullable and pushable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return secureexec.Command("echo", "10")
}
return secureexec.Command("echo", "11")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "11", pushableCount)
assert.EqualValues(t, "10", pullableCount)
},
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
Expect("git rev-list HEAD..@{u} --count", "2\n", nil),
"1", "2",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
gitCmd := NewDummyGitCommandWithRunner(s.runner)
pushables, pullables := gitCmd.GetCommitDifferences("HEAD", "@{u}")
assert.EqualValues(t, s.expectedPushables, pushables)
assert.EqualValues(t, s.expectedPullables, pullables)
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandNewBranch is a function.
func TestGitCommandNewBranch(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
Expect(`git checkout -b "test" "master"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.NewBranch("test", "master"))
runner.CheckForMissingCalls()
}
// TestGitCommandDeleteBranch is a function.
func TestGitCommandDeleteBranch(t *testing.T) {
type scenario struct {
testName string
branch string
force bool
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"Delete a branch",
"test",
false,
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-d", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git branch -d "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
{
"Force delete a branch",
"test",
true,
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-D", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git branch -D "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
@@ -123,31 +88,27 @@ func TestGitCommandDeleteBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.DeleteBranch(s.branch, s.force))
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.DeleteBranch("test", s.force))
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandMerge is a function.
func TestGitCommandMerge(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
Expect(`git merge --no-edit "test"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
runner.CheckForMissingCalls()
}
// TestGitCommandCheckout is a function.
func TestGitCommandCheckout(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
force bool
}
@@ -155,12 +116,7 @@ func TestGitCommandCheckout(t *testing.T) {
scenarios := []scenario{
{
"Checkout",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git checkout "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
@@ -168,12 +124,7 @@ func TestGitCommandCheckout(t *testing.T) {
},
{
"Checkout forced",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "--force", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git checkout --force "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
@@ -183,52 +134,43 @@ func TestGitCommandCheckout(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandGetBranchGraph is a function.
func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).ExpectArgs([]string{
"git", "log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--",
}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
_, err := gitCmd.GetBranchGraph("test")
assert.NoError(t, err)
}
func TestGitCommandGetAllBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args)
return secureexec.Command("echo")
}
cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd
_, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr)
runner := oscommands.NewFakeRunner(t).ExpectArgs([]string{
"git", "log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium",
}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
cmdStr := gitCmd.UserConfig.Git.AllBranchesLogCmd
_, err := gitCmd.Cmd.New(cmdStr).RunWithOutput()
assert.NoError(t, err)
}
// TestGitCommandCurrentBranchName is a function.
func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(string, string, error)
}
scenarios := []scenario{
{
"says we are on the master branch if we are",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return secureexec.Command("echo", "master")
},
oscommands.NewFakeRunner(t).Expect(`git symbolic-ref --short HEAD`, "master", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
@@ -237,20 +179,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
},
{
"falls back to git `git branch --contains` if symbolic-ref fails",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return secureexec.Command("echo", "* master")
}
return nil
},
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "* master", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
@@ -259,20 +190,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
},
{
"handles a detached head",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
}
return nil
},
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "* (HEAD detached at 123abcd)", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123abcd", name)
@@ -281,10 +201,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return secureexec.Command("test")
},
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "", errors.New("error")),
func(name string, displayname string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", name)
@@ -295,19 +214,18 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CurrentBranchName())
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandResetHard is a function.
func TestGitCommandResetHard(t *testing.T) {
type scenario struct {
testName string
ref string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
@@ -315,23 +233,17 @@ func TestGitCommandResetHard(t *testing.T) {
{
"valid case",
"HEAD",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git reset --hard HEAD`,
Replace: "echo",
},
}),
oscommands.NewFakeRunner(t).
Expect(`git reset --hard "HEAD"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.ResetHard(s.ref))
})
}
+26 -19
View File
@@ -10,15 +10,20 @@ import (
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
return c.Cmd.New("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name)).Run()
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string, options oscommands.RunCommandOptions) error {
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string) error {
return c.Cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
AddEnvVars(envVars...).
Run()
}
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdObj {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
@@ -30,51 +35,53 @@ func (c *GitCommand) CommitCmdStr(message string, flags string) string {
flagsStr = fmt.Sprintf(" %s", flags)
}
return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)
return c.Cmd.New(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs))
}
// Get the subject of the HEAD commit
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
cmdStr := "git log -1 --pretty=%s"
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
message, err := c.Cmd.New("git log -1 --pretty=%s").RunWithOutput()
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
messageWithHeader, err := c.Cmd.New(cmdStr).RunWithOutput()
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
return c.RunCommandWithOutput("git show --no-patch --pretty=format:%%s %s", sha)
return c.Cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).RunWithOutput()
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() error {
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
return c.AmendHeadCmdObj().Run()
}
func (c *GitCommand) AmendHeadCmdStr() string {
return "git commit --amend --no-edit --allow-empty"
func (c *GitCommand) AmendHeadCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git commit --amend --no-edit --allow-empty")
}
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
contextSize := c.UserConfig.Git.DiffContextSize
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
}
return fmt.Sprintf("git show --submodule --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", c.colorArg(), contextSize, sha, filterPathArg)
return c.Cmd.New(cmdStr)
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.RunCommand("git revert %s", sha)
return c.Cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
}
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error {
return c.RunCommand("git revert %s -m %d", sha, parentNumber)
return c.Cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run()
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
@@ -84,15 +91,15 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
cmdObj, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
return c.RunCommand("git commit --fixup=%s", sha)
return c.Cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
}
+59 -37
View File
@@ -1,43 +1,31 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
// TestGitCommandRenameCommit is a function.
func TestGitCommandRenameCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.RenameCommit("test"))
runner.CheckForMissingCalls()
}
// TestGitCommandResetToCommit is a function.
func TestGitCommandResetToCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "reset", "--hard", "78976bc"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", []string{}))
runner.CheckForMissingCalls()
}
// TestGitCommandCommitStr is a function.
func TestGitCommandCommitStr(t *testing.T) {
func TestGitCommandCommitObj(t *testing.T) {
gitCmd := NewDummyGitCommand()
type scenario struct {
@@ -70,43 +58,77 @@ func TestGitCommandCommitStr(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
cmdStr := gitCmd.CommitCmdObj(s.message, s.flags).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
}
// TestGitCommandCreateFixupCommit is a function.
func TestGitCommandCreateFixupCommit(t *testing.T) {
type scenario struct {
testName string
sha string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"valid case",
"12345",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git commit --fixup=12345`,
Replace: "echo",
},
}),
func(err error) {
testName: "valid case",
sha: "12345",
runner: oscommands.NewFakeRunner(t).
Expect(`git commit --fixup=12345`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CreateFixupCommit(s.sha))
s.runner.CheckForMissingCalls()
})
}
}
func TestGitCommandShowCmdObj(t *testing.T) {
type scenario struct {
testName string
filterPath string
contextSize int
expected string
}
gitCmd := NewDummyGitCommand()
scenarios := []scenario{
{
testName: "Default case without filter path",
filterPath: "",
contextSize: 3,
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 ",
},
{
testName: "Default case with filter path",
filterPath: "file.txt",
contextSize: 3,
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 -- " + gitCmd.OSCommand.Quote("file.txt"),
},
{
testName: "Show diff with custom context size",
filterPath: "",
contextSize: 77,
expected: "git show --submodule --color=always --unified=77 --no-renames --stat -p 1234567890 ",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
cmdStr := gitCmd.ShowCmdObj("1234567890", s.filterPath).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
}
+4 -4
View File
@@ -20,7 +20,7 @@ func (c *GitCommand) ConfiguredPager() string {
}
func (c *GitCommand) GetPager(width int) string {
useConfig := c.Config.GetUserConfig().Git.Paging.UseConfig
useConfig := c.UserConfig.Git.Paging.UseConfig
if useConfig {
pager := c.ConfiguredPager()
return strings.Split(pager, "| less")[0]
@@ -30,18 +30,18 @@ func (c *GitCommand) GetPager(width int) string {
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := c.Config.GetUserConfig().Git.Paging.Pager
pagerTemplate := c.UserConfig.Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (c *GitCommand) colorArg() string {
return c.Config.GetUserConfig().Git.Paging.ColorArg
return c.UserConfig.Git.Paging.ColorArg
}
// UsingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) UsingGpg() bool {
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
overrideGpg := c.UserConfig.Git.OverrideGpg
if overrideGpg {
return false
}
+10 -6
View File
@@ -6,8 +6,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -18,13 +16,19 @@ func NewDummyGitCommand() *GitCommand {
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
newAppConfig := config.NewDummyAppConfig()
return &GitCommand{
Log: utils.NewDummyLog(),
Common: utils.NewDummyCommon(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
Config: newAppConfig,
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
}
func NewDummyGitCommandWithRunner(runner oscommands.ICmdObjRunner) *GitCommand {
builder := oscommands.NewDummyCmdObjBuilder(runner)
gitCommand := NewDummyGitCommand()
gitCommand.Cmd = builder
gitCommand.OSCommand.Cmd = builder
return gitCommand
}
+42 -33
View File
@@ -9,7 +9,9 @@ import (
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -23,27 +25,27 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
return string(buf), nil
}
func (c *GitCommand) OpenMergeToolCmd() string {
return "git mergetool"
func (c *GitCommand) OpenMergeToolCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git mergetool")
}
func (c *GitCommand) OpenMergeTool() error {
return c.OSCommand.RunCommand("git mergetool")
return c.OpenMergeToolCmdObj().Run()
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
return c.Cmd.New("git add -- " + c.OSCommand.Quote(fileName)).Run()
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.RunCommand("git add -A")
return c.Cmd.New("git add -A").Run()
}
// UnstageAll unstages all files
func (c *GitCommand) UnstageAll() error {
return c.RunCommand("git reset")
return c.Cmd.New("git reset").Run()
}
// UnStageFile unstages a file
@@ -56,7 +58,8 @@ func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
}
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
err := c.Cmd.New(fmt.Sprintf(command, c.OSCommand.Quote(name))).Run()
if err != nil {
return err
}
}
@@ -73,7 +76,10 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
filesWithoutRenames := loaders.
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
@@ -120,22 +126,22 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
if file.ShortStatus == "AA" {
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
if err := c.Cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
return err
}
if err := c.RunCommand("git add -- %s", quotedFileName); err != nil {
if err := c.Cmd.New("git add -- " + quotedFileName).Run(); err != nil {
return err
}
return nil
}
if file.ShortStatus == "DU" {
return c.RunCommand("git rm -- %s", quotedFileName)
return c.Cmd.New("git rm -- " + quotedFileName).Run()
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
if err := c.Cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
return err
}
}
@@ -161,7 +167,7 @@ func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
}
quotedPath := c.OSCommand.Quote(node.GetPath())
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
if err := c.Cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
return err
}
@@ -186,7 +192,7 @@ func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.RunCommand("git checkout -- %s", quotedFileName)
return c.Cmd.New("git checkout -- " + quotedFileName).Run()
}
// Ignore adds a file to the gitignore for the repo
@@ -197,16 +203,17 @@ func (c *GitCommand) Ignore(filename string) error {
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
s, _ := c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) string {
func (c *GitCommand) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
quotedPath := c.OSCommand.Quote(node.GetPath())
ignoreWhitespaceArg := ""
contextSize := c.UserConfig.Git.DiffContextSize
if cached {
cachedArg = "--cached"
}
@@ -220,11 +227,13 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cache
ignoreWhitespaceArg = "--ignore-all-space"
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s %s %s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
return c.Cmd.New(cmdStr)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(c.Config.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
@@ -235,18 +244,18 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
flagStr += " --" + flag
}
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
return c.Cmd.New(fmt.Sprintf("git apply%s %s", flagStr, c.OSCommand.Quote(filepath))).Run()
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
return c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
}
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
func (c *GitCommand) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
colorArg := c.colorArg()
contextSize := c.UserConfig.Git.DiffContextSize
if plain {
colorArg = "never"
}
@@ -256,12 +265,12 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))
return c.Cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s", contextSize, colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName)))
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
return c.RunCommand("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))
return c.Cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))).Run()
}
// DiscardOldFileChanges discards changes to a file from an old commit
@@ -271,7 +280,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.RunCommand("git cat-file -e HEAD^:%s", c.OSCommand.Quote(fileName)); err != nil {
if err := c.Cmd.New("git cat-file -e HEAD^:" + c.OSCommand.Quote(fileName)).Run(); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
@@ -294,28 +303,28 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.RunCommand("git checkout -- .")
return c.Cmd.New("git checkout -- .").Run()
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.RunCommand("git rm -r --cached -- %s", c.OSCommand.Quote(name))
return c.Cmd.New("git rm -r --cached -- " + c.OSCommand.Quote(name)).Run()
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.RunCommand("git clean -fd")
return c.Cmd.New("git clean -fd").Run()
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
submoduleConfigs, err := c.GetSubmoduleConfigs()
submoduleConfigs, err := c.Submodules.GetConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
if err := c.Submodules.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
@@ -328,7 +337,7 @@ func (c *GitCommand) ResetAndClean() error {
}
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
editor := c.Config.GetUserConfig().OS.EditCommand
editor := c.UserConfig.OS.EditCommand
if editor == "" {
editor = c.GitConfig.Get("core.editor")
@@ -344,7 +353,7 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
editor = c.OSCommand.Getenv("EDITOR")
}
if editor == "" {
if err := c.OSCommand.RunCommand("which vi"); err == nil {
if err := c.OSCommand.Cmd.New("which vi").Run(); err == nil {
editor = "vi"
}
}
@@ -358,6 +367,6 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
"line": strconv.Itoa(lineNumber),
}
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
editCmdTemplate := c.UserConfig.OS.EditCommandTemplate
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
}
+357 -491
View File
File diff suppressed because it is too large Load Diff
+46 -45
View File
@@ -6,19 +6,17 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// this takes something like:
@@ -27,17 +25,27 @@ import (
// and returns '264fc6f5' as the second match
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
type Loaders struct {
Commits *loaders.CommitLoader
Branches *loaders.BranchLoader
Files *loaders.FileLoader
CommitFiles *loaders.CommitFileLoader
Remotes *loaders.RemoteLoader
ReflogCommits *loaders.ReflogCommitLoader
Stash *loaders.StashLoader
Tags *loaders.TagLoader
}
// GitCommand is our main git interface
type GitCommand struct {
Log *logrus.Entry
*common.Common
OSCommand *oscommands.OSCommand
Repo *gogit.Repository
Tr *i18n.TranslationSet
Config config.AppConfigurer
DotGitDir string
onSuccessfulContinue func() error
PatchManager *patch.PatchManager
GitConfig git_config.IGitConfig
Loaders Loaders
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
PushToCurrent bool
@@ -46,14 +54,16 @@ type GitCommand struct {
// Coincidentally at the moment it's the same view that OnRunCommand logs to
// but that need not always be the case.
GetCmdWriter func() io.Writer
Cmd oscommands.ICmdObjBuilder
Submodules SubmoduleCommands
}
// NewGitCommand it runs git commands
func NewGitCommand(
log *logrus.Entry,
cmn *common.Common,
osCommand *oscommands.OSCommand,
tr *i18n.TranslationSet,
config config.AppConfigurer,
gitConfig git_config.IGitConfig,
) (*GitCommand, error) {
var repo *gogit.Repository
@@ -65,7 +75,7 @@ func NewGitCommand(
}
var err error
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
if repo, err = setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr); err != nil {
return nil, err
}
@@ -74,19 +84,33 @@ func NewGitCommand(
return nil, err
}
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
gitCommand := &GitCommand{
Log: log,
Common: cmn,
OSCommand: osCommand,
Tr: tr,
Repo: repo,
Config: config,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
GitConfig: gitConfig,
GetCmdWriter: func() io.Writer { return ioutil.Discard },
Cmd: cmd,
}
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
gitCommand.Loaders = Loaders{
Commits: loaders.NewCommitLoader(cmn, gitCommand),
Branches: loaders.NewBranchLoader(cmn, gitCommand),
Files: loaders.NewFileLoader(cmn, cmd, gitConfig),
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
Remotes: loaders.NewRemoteLoader(cmn, cmd, gitCommand.Repo.Remotes),
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
Stash: loaders.NewStashLoader(cmn, cmd),
Tags: loaders.NewTagLoader(cmn, cmd),
}
gitCommand.Submodules = NewSubmoduleCommands(cmn, cmd, dotGitDir)
gitCommand.PatchManager = patch.NewPatchManager(gitCommand.Log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
return gitCommand, nil
}
@@ -103,6 +127,8 @@ func (c *GitCommand) WithSpan(span string) *GitCommand {
*newGitCommand = *c
newGitCommand.OSCommand = c.OSCommand.WithSpan(span)
newGitCommand.Cmd = NewGitCmdObjBuilder(c.Log, newGitCommand.OSCommand.Cmd)
// NOTE: unlike the other things here which create shallow clones, this will
// actually update the PatchManager on the original struct to have the new span.
// This means each time we call ApplyPatch in PatchManager, we need to ensure
@@ -223,38 +249,13 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
}
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.RunCommand("git rev-parse --git-dir")
return osCommand.Cmd.New("git rev-parse --git-dir").Run()
}
func (c *GitCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
return err
func (c *GitCommand) GetDotGitDir() string {
return c.DotGitDir
}
func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
// TODO: have this retry logic in other places we run the command
waitTime := 50 * time.Millisecond
retryCount := 5
attempt := 0
for {
output, err := c.OSCommand.RunCommandWithOutput(formatString, formatArgs...)
if err != nil {
// if we have an error based on the index lock, we should wait a bit and then retry
if strings.Contains(output, ".git/index.lock") {
c.Log.Error(output)
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
attempt++
time.Sleep(waitTime)
if attempt < retryCount {
continue
}
}
}
return output, err
}
}
func (c *GitCommand) NewCmdObjFromStr(cmdStr string) oscommands.ICmdObj {
return c.OSCommand.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
func (c *GitCommand) GetCmd() oscommands.ICmdObjBuilder {
return c.Cmd
}
+47
View File
@@ -0,0 +1,47 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/sirupsen/logrus"
)
// all we're doing here is wrapping the default command object builder with
// some git-specific stuff: e.g. adding a git-specific env var
type gitCmdObjBuilder struct {
innerBuilder *oscommands.CmdObjBuilder
}
var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder) *gitCmdObjBuilder {
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
return &gitCmdObjRunner{
log: log,
innerRunner: runner,
}
})
return &gitCmdObjBuilder{
innerBuilder: updatedBuilder,
}
}
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
func (self *gitCmdObjBuilder) New(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.New(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewFromArgs(args []string) oscommands.ICmdObj {
return self.innerBuilder.NewFromArgs(args).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) Quote(str string) string {
return self.innerBuilder.Quote(str)
}
+49
View File
@@ -0,0 +1,49 @@
package commands
import (
"strings"
"time"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/sirupsen/logrus"
)
// here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock
type gitCmdObjRunner struct {
log *logrus.Entry
innerRunner oscommands.ICmdObjRunner
}
func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
// TODO: have this retry logic in other places we run the command
waitTime := 50 * time.Millisecond
retryCount := 5
attempt := 0
for {
output, err := self.innerRunner.RunWithOutput(cmdObj)
if err != nil {
// if we have an error based on the index lock, we should wait a bit and then retry
if strings.Contains(output, ".git/index.lock") {
self.log.Error(output)
self.log.Info("index.lock prevented command from running. Retrying command after a small wait")
attempt++
time.Sleep(waitTime)
if attempt < retryCount {
continue
}
}
}
return output, err
}
}
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
}
+1 -4
View File
@@ -10,8 +10,6 @@ import (
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
@@ -210,8 +208,7 @@ func TestNewGitCommand(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.setup()
newAppConfig := config.NewDummyAppConfig()
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, git_config.NewFakeGitConfig(nil)))
s.test(NewGitCommand(utils.NewDummyCommon(), oscommands.NewDummyOSCommand(), git_config.NewFakeGitConfig(nil)))
})
}
}
@@ -0,0 +1,54 @@
package hosting_service
// if you want to make a custom regex for a given service feel free to test it out
// at regoio.herokuapp.com
var defaultUrlRegexStrings = []string{
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
}
// we've got less type safety using go templates but this lends itself better to
// users adding custom service definitions in their config
var githubServiceDef = ServiceDefinition{
provider: "github",
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1",
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1",
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
}
var bitbucketServiceDef = ServiceDefinition{
provider: "bitbucket",
pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1",
pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1",
commitURL: "/commits/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
}
var gitLabServiceDef = ServiceDefinition{
provider: "gitlab",
pullRequestURLIntoDefaultBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}",
pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
commitURL: "/commit/{{.CommitSha}}",
regexStrings: defaultUrlRegexStrings,
}
var serviceDefinitions = []ServiceDefinition{githubServiceDef, bitbucketServiceDef, gitLabServiceDef}
var defaultServiceDomains = []ServiceDomain{
{
serviceDefinition: githubServiceDef,
gitDomain: "github.com",
webDomain: "github.com",
},
{
serviceDefinition: bitbucketServiceDef,
gitDomain: "bitbucket.org",
webDomain: "bitbucket.org",
},
{
serviceDefinition: gitLabServiceDef,
gitDomain: "gitlab.com",
webDomain: "gitlab.com",
},
}
@@ -0,0 +1,201 @@
package hosting_service
import (
"fmt"
"regexp"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
// and this package's responsibility is to determine which service you're using based on the remote URL,
// and then which URL you need for whatever use case you have.
type HostingServiceMgr struct {
log logrus.FieldLogger
tr *i18n.TranslationSet
remoteURL string // e.g. https://github.com/jesseduffield/lazygit
// see https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls
configServiceDomains map[string]string
}
// NewHostingServiceMgr creates new instance of PullRequest
func NewHostingServiceMgr(log logrus.FieldLogger, tr *i18n.TranslationSet, remoteURL string, configServiceDomains map[string]string) *HostingServiceMgr {
return &HostingServiceMgr{
log: log,
tr: tr,
remoteURL: remoteURL,
configServiceDomains: configServiceDomains,
}
}
func (self *HostingServiceMgr) GetPullRequestURL(from string, to string) (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
if to == "" {
return gitService.getPullRequestURLIntoDefaultBranch(from), nil
} else {
return gitService.getPullRequestURLIntoTargetBranch(from, to), nil
}
}
func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) {
gitService, err := self.getService()
if err != nil {
return "", err
}
pullRequestURL := gitService.getCommitURL(commitSha)
return pullRequestURL, nil
}
func (self *HostingServiceMgr) getService() (*Service, error) {
serviceDomain, err := self.getServiceDomain(self.remoteURL)
if err != nil {
return nil, err
}
root, err := serviceDomain.getRootFromRemoteURL(self.remoteURL)
if err != nil {
return nil, err
}
return &Service{
root: root,
ServiceDefinition: serviceDomain.serviceDefinition,
}, nil
}
func (self *HostingServiceMgr) getServiceDomain(repoURL string) (*ServiceDomain, error) {
candidateServiceDomains := self.getCandidateServiceDomains()
for _, serviceDomain := range candidateServiceDomains {
// I feel like it makes more sense to see if the repo url contains the service domain's git domain,
// but I don't want to break anything by changing that right now.
if strings.Contains(repoURL, serviceDomain.serviceDefinition.provider) {
return &serviceDomain, nil
}
}
return nil, errors.New(self.tr.UnsupportedGitService)
}
func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
serviceDefinitionByProvider := map[string]ServiceDefinition{}
for _, serviceDefinition := range serviceDefinitions {
serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition
}
var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains))
copy(serviceDomains, defaultServiceDomains)
if len(self.configServiceDomains) > 0 {
for gitDomain, typeAndDomain := range self.configServiceDomains {
splitData := strings.Split(typeAndDomain, ":")
if len(splitData) != 2 {
self.log.Errorf("Unexpected format for git service: '%s'. Expected something like 'github.com:github.com'", typeAndDomain)
continue
}
provider := splitData[0]
webDomain := splitData[1]
serviceDefinition, ok := serviceDefinitionByProvider[provider]
if !ok {
providerNames := []string{}
for _, serviceDefinition := range serviceDefinitions {
providerNames = append(providerNames, serviceDefinition.provider)
}
self.log.Errorf("Unknown git service type: '%s'. Expected one of %s", provider, strings.Join(providerNames, ", "))
continue
}
serviceDomains = append(serviceDomains, ServiceDomain{
gitDomain: gitDomain,
webDomain: webDomain,
serviceDefinition: serviceDefinition,
})
}
}
return serviceDomains
}
// a service domains pairs a service definition with the actual domain it's being served from.
// Sometimes the git service is hosted in a custom domains so although it'll use say
// the github service definition, it'll actually be served from e.g. my-custom-github.com
type ServiceDomain struct {
gitDomain string // the one that appears in the git remote url
webDomain string // the one that appears in the web url
serviceDefinition ServiceDefinition
}
func (self ServiceDomain) getRootFromRemoteURL(repoURL string) (string, error) {
// we may want to make this more specific to the service in future e.g. if
// some new service comes along which has a different root url structure.
repoInfo, err := self.serviceDefinition.getRepoInfoFromURL(repoURL)
if err != nil {
return "", err
}
return fmt.Sprintf("https://%s/%s/%s", self.webDomain, repoInfo.Owner, repoInfo.Repository), nil
}
// RepoInformation holds some basic information about the repo
type RepoInformation struct {
Owner string
Repository string
}
type ServiceDefinition struct {
provider string
pullRequestURLIntoDefaultBranch string
pullRequestURLIntoTargetBranch string
commitURL string
regexStrings []string
}
func (self ServiceDefinition) getRepoInfoFromURL(url string) (*RepoInformation, error) {
for _, regexStr := range self.regexStrings {
re := regexp.MustCompile(regexStr)
matches := utils.FindNamedMatches(re, url)
if matches != nil {
return &RepoInformation{
Owner: matches["owner"],
Repository: matches["repo"],
}, nil
}
}
return nil, errors.New("Failed to parse repo information from url")
}
type Service struct {
root string
ServiceDefinition
}
func (self *Service) getPullRequestURLIntoDefaultBranch(from string) string {
return self.resolveUrl(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from})
}
func (self *Service) getPullRequestURLIntoTargetBranch(from string, to string) string {
return self.resolveUrl(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to})
}
func (self *Service) getCommitURL(commitSha string) string {
return self.resolveUrl(self.commitURL, map[string]string{"CommitSha": commitSha})
}
func (self *Service) resolveUrl(templateString string, args map[string]string) string {
return self.root + utils.ResolvePlaceholderString(templateString, args)
}
@@ -0,0 +1,233 @@
package hosting_service
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
func TestGetRepoInfoFromURL(t *testing.T) {
type scenario struct {
serviceDefinition ServiceDefinition
testName string
repoURL string
test func(*RepoInformation)
}
scenarios := []scenario{
{
githubServiceDef,
"Returns repository information for git remote url",
"git@github.com:petersmith/super_calculator",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "petersmith")
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
},
},
{
githubServiceDef,
"Returns repository information for git remote url, trimming trailing '.git'",
"git@github.com:petersmith/super_calculator.git",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "petersmith")
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
},
},
{
githubServiceDef,
"Returns repository information for ssh remote url",
"ssh://git@github.com/petersmith/super_calculator",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "petersmith")
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
},
},
{
githubServiceDef,
"Returns repository information for http remote url",
"https://my_username@bitbucket.org/johndoe/social_network.git",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "johndoe")
assert.EqualValues(t, repoInfo.Repository, "social_network")
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result, err := s.serviceDefinition.getRepoInfoFromURL(s.repoURL)
assert.NoError(t, err)
s.test(result)
})
}
}
func TestGetPullRequestURL(t *testing.T) {
type scenario struct {
testName string
from string
to string
remoteUrl string
configServiceDomains map[string]string
test func(url string, err error)
expectedLoggedErrors []string
}
scenarios := []scenario{
{
testName: "Opens a link to new pull request on bitbucket",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url",
from: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github",
from: "feature/sum-operation",
remoteUrl: "git@github.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with specific target branch",
from: "feature/profile-page/avatar",
to: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
from: "feature/remote-events",
to: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "git@github.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on gitlab",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab in nested groups",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
remoteUrl: "git@something.com:peter/calculator.git",
test: func(url string, err error) {
assert.EqualError(t, err, "Unsupported git service")
},
},
{
testName: "Does not log error when config service domains are valid",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
configServiceDomains: map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
expectedLoggedErrors: nil,
},
{
testName: "Logs error when config service domain is malformed",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
configServiceDomains: map[string]string{
"noservice.work.com": "noservice.work.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
expectedLoggedErrors: []string{"Unexpected format for git service: 'noservice.work.com'. Expected something like 'github.com:github.com'"},
},
{
testName: "Logs error when config service domain uses unknown provider",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
configServiceDomains: map[string]string{
"invalid.work.com": "noservice:invalid.work.com",
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab"},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
tr := i18n.EnglishTranslationSet()
log := &test.FakeFieldLogger{}
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
log.AssertErrors(t, s.expectedLoggedErrors)
})
}
}
@@ -1,12 +1,12 @@
package commands
package loaders
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
@@ -20,25 +20,76 @@ import (
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
ReflogCommits []*models.Commit
// BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct {
*common.Common
getRawBranches func() (string, error)
getCurrentBranchName func() (string, string, error)
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*models.Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
type BranchLoaderGitCommand interface {
GetRawBranches() (string, error)
CurrentBranchName() (string, string, error)
}
func (b *BranchListBuilder) obtainBranches() []*models.Branch {
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
func NewBranchLoader(
cmn *common.Common,
gitCommand BranchLoaderGitCommand,
) *BranchLoader {
return &BranchLoader{
Common: cmn,
getRawBranches: gitCommand.GetRawBranches,
getCurrentBranchName: gitCommand.CurrentBranchName,
}
}
// Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) []*models.Branch {
branches := self.obtainBranches()
reflogBranches := self.obtainReflogBranches(reflogCommits)
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*models.Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
output, err := self.getRawBranches()
if err != nil {
panic(err)
}
@@ -98,58 +149,13 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch {
return branches
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*models.Branch {
branches := b.obtainBranches()
reflogBranches := b.obtainReflogBranches()
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*models.Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch {
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
foundBranchesMap := map[string]bool{}
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(b.ReflogCommits))
for _, commit := range b.ReflogCommits {
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
for _, commit := range reflogCommits {
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
+57
View File
@@ -0,0 +1,57 @@
package loaders
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type CommitFileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
return &CommitFileLoader{
Common: common,
cmd: cmd,
}
}
// GetFilesInDiff get the specified commit files
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).RunWithOutput()
if err != nil {
return nil, err
}
return self.getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (self *CommitFileLoader) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// typical result looks like 'A my_file' meaning my_file was added
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Name: name,
ChangeStatus: changeStatus,
})
}
return commitFiles
}
@@ -1,10 +1,9 @@
package commands
package loaders
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
@@ -12,9 +11,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// context:
@@ -23,39 +22,146 @@ import (
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *oscommands.OSCommand
Tr *i18n.TranslationSet
// CommitLoader returns a list of Commit objects for the current repo
type CommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getCurrentBranchName func() (string, string, error)
getRebaseMode func() (enums.RebaseMode, error)
readFile func(filename string) ([]byte, error)
walkFiles func(root string, fn filepath.WalkFunc) error
dotGitDir string
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(
log *logrus.Entry,
gitCommand *GitCommand,
osCommand *oscommands.OSCommand,
tr *i18n.TranslationSet,
) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
type CommitLoaderGitCommand interface {
CurrentBranchName() (string, string, error)
RebaseMode() (enums.RebaseMode, error)
GetCmd() oscommands.ICmdObjBuilder
GetDotGitDir() string
}
// making our dependencies explicit for the sake of easier testing
func NewCommitLoader(
cmn *common.Common,
gitCommand CommitLoaderGitCommand,
) *CommitLoader {
return &CommitLoader{
Common: cmn,
cmd: gitCommand.GetCmd(),
getCurrentBranchName: gitCommand.CurrentBranchName,
getRebaseMode: gitCommand.RebaseMode,
readFile: ioutil.ReadFile,
walkFiles: filepath.Walk,
dotGitDir: gitCommand.GetDotGitDir(),
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
// GetCommits obtains the commits of the current branch
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = self.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
if canExtractCommit(line) {
commit := self.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
if len(commits) == 0 {
return commits, nil
}
if rebaseMode != enums.REBASE_MODE_NONE {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == enums.REBASE_MODE_NONE {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
@@ -88,104 +194,8 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == "" {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := c.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = c.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
cmd := c.getLogCmd(opts)
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
if canExtractCommit(line) {
commit := c.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", c.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
commits, err := c.getRebasingCommits(rebaseMode)
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
commits, err := self.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
@@ -201,7 +211,7 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo
// note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage
cmd := c.OSCommand.ExecutableFromString(
cmdObj := self.cmd.New(
fmt.Sprintf(
"git show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
@@ -212,9 +222,9 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
if canExtractCommit(line) {
commit := c.extractCommitFromLine(line)
commit := self.extractCommitFromLine(line)
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
@@ -230,20 +240,20 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
switch rebaseMode {
case REBASE_MODE_MERGING:
return c.getNormalRebasingCommits()
case REBASE_MODE_INTERACTIVE:
return c.getInteractiveRebasingCommits()
case enums.REBASE_MODE_MERGING:
return self.getNormalRebasingCommits()
case enums.REBASE_MODE_INTERACTIVE:
return self.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) {
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply/rewritten"))
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
@@ -251,7 +261,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
@@ -263,12 +273,12 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
bytesContent, err := self.readFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
commit, err := self.commitFromPatch(content)
if err != nil {
return err
}
@@ -294,10 +304,10 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo"))
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
c.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
@@ -328,7 +338,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, e
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, error) {
func (self *CommitLoader) commitFromPatch(content string) (*models.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
@@ -339,8 +349,8 @@ func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, err
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := c.getMergeBase(refName)
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := self.getMergeBase(refName)
if err != nil {
return nil, err
}
@@ -362,8 +372,8 @@ func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*m
return commits, nil
}
func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
currentBranch, _, err := c.GitCommand.CurrentBranchName()
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
currentBranch, _, err := self.getCurrentBranchName()
if err != nil {
return "", err
}
@@ -374,7 +384,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch))
output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).RunWithOutput()
return ignoringWarnings(output), nil
}
@@ -390,8 +400,12 @@ func ignoringWarnings(commandOutput string) string {
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName))
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
output, err := self.cmd.
New(
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
).
RunWithOutput()
if err != nil {
return "", err
}
@@ -400,18 +414,18 @@ func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error)
}
// getLog gets the git log.
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag := ""
if opts.Limit {
limitFlag = "-300"
limitFlag = " -300"
}
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
}
config := c.GitCommand.Config.GetUserConfig().Git.Log
config := self.UserConfig.Git.Log
orderFlag := "--" + config.Order
allFlag := ""
@@ -419,10 +433,10 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
allFlag = " --all"
}
return c.OSCommand.ExecutableFromString(
return self.cmd.New(
fmt.Sprintf(
"git log %s %s %s --oneline %s %s --abbrev=%d %s",
c.OSCommand.Quote(opts.RefName),
"git log %s %s %s --oneline %s%s --abbrev=%d%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
@@ -443,5 +457,5 @@ var prettyFormat = fmt.Sprintf(
)
func canExtractCommit(line string) bool {
return strings.Split(line, " ")[0] != "gpg:"
return line != "" && strings.Split(line, " ")[0] != "gpg:"
}
+212
View File
@@ -0,0 +1,212 @@
package loaders
import (
"path/filepath"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func NewDummyCommitLoader() *CommitLoader {
cmn := utils.NewDummyCommon()
return &CommitLoader{
Common: cmn,
cmd: nil,
getCurrentBranchName: func() (string, string, error) { return "master", "master", nil },
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_NONE, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
}
const commitsOutput = `0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield||d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield||985fe482e806b172aea4|refactoring the config struct`
func TestGetCommits(t *testing.T) {
type scenario struct {
testName string
runner oscommands.ICmdObjRunner
expectedCommits []*models.Commit
expectedError error
rebaseMode enums.RebaseMode
currentBranchName string
opts GetCommitsOptions
}
scenarios := []scenario{
{
testName: "should return no commits if there are none",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
{
testName: "should return commits if they are present",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
// here it's seeing which commits are yet to be pushed
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, commitsOutput, nil).
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
expectedCommits: []*models.Commit{
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: "unpushed",
Action: "",
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
Author: "Jesse Duffield",
UnixTimestamp: 1640826609,
Parents: []string{
"b21997d6b4cbdf84b149",
},
},
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
Author: "Jesse Duffield",
UnixTimestamp: 1640824515,
Parents: []string{
"e94e8fc5b6fab4cb755f",
},
},
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640823749,
Parents: []string{
"d8084cd558925eb7c9c3",
},
},
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640821426,
Parents: []string{
"65f910ebd85283b5cce9",
},
},
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640821275,
Parents: []string{
"26c07b1ab33860a1a759",
},
},
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640750752,
Parents: []string{
"3d4470a6c072208722e5",
},
},
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640748818,
Parents: []string{
"053a66a7be3da43aacdc",
},
},
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640739815,
Parents: []string{
"985fe482e806b172aea4",
},
},
},
expectedError: nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
builder := &CommitLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
getCurrentBranchName: func() (string, string, error) {
return scenario.currentBranchName, scenario.currentBranchName, nil
},
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
commits, err := builder.GetCommits(scenario.opts)
assert.Equal(t, scenario.expectedCommits, commits)
assert.Equal(t, scenario.expectedError, err)
})
}
}
@@ -1,36 +1,54 @@
package commands
package loaders
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GetStatusFiles git status files
type FileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
gitConfig git_config.IGitConfig
getFileType func(string) string
}
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, gitConfig git_config.IGitConfig) *FileLoader {
return &FileLoader{
Common: cmn,
cmd: cmd,
gitConfig: gitConfig,
getFileType: oscommands.FileType,
}
}
type GetStatusFileOptions struct {
NoRenames bool
}
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
untrackedFilesSetting := self.gitConfig.Get("status.showUntrackedFiles")
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
c.Log.Error(err)
self.Log.Error(err)
}
files := []*models.File{}
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
continue
}
change := status.Change
@@ -52,7 +70,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
Added: unstagedChange == "A" || untracked,
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(status.Name),
Type: self.getFileType(status.Name),
ShortStatus: change,
}
files = append(files, file)
@@ -74,13 +92,13 @@ type FileStatus struct {
PreviousName string
}
func (c *GitCommand) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = "--no-renames"
noRenamesFlag = " --no-renames"
}
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
statusLines, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).RunWithOutput()
if err != nil {
return []FileStatus{}, err
}
+203
View File
@@ -0,0 +1,203 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
runner oscommands.ICmdObjRunner
expectedFiles []*models.File
}
scenarios := []scenario{
{
"No files found",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
[]*models.File{},
},
{
"Several files found",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
nil,
),
[]*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "file",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "file",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "file",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "file",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "file",
ShortStatus: "UU",
},
},
},
{
"File with new line char",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
[]*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "file",
ShortStatus: "MM",
},
},
},
{
"Renamed files",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
nil,
),
[]*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "file",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "file",
ShortStatus: "RM",
},
},
},
{
"File with arrow in name",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
`?? a -> b.txt`,
nil,
),
[]*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "file",
ShortStatus: "??",
},
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
gitConfig := git_config.NewFakeGitConfig(map[string]string{"status.showUntrackedFiles": "yes"})
loader := &FileLoader{
Common: utils.NewDummyCommon(),
cmd: cmd,
gitConfig: gitConfig,
getFileType: func(string) string { return "file" },
}
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
})
}
}
@@ -1,4 +1,4 @@
package commands
package loaders
import (
"fmt"
@@ -7,21 +7,34 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type ReflogCommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
return &ReflogCommitLoader{
Common: common,
cmd: cmd,
}
}
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
}
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
cmdObj := self.cmd.New(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
onlyObtainedNewReflogCommits := false
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
fields := strings.SplitN(line, " ", 3)
if len(fields) <= 2 {
return false, nil
@@ -1,4 +1,4 @@
package commands
package loaders
import (
"fmt"
@@ -6,18 +6,37 @@ import (
"sort"
"strings"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
// get remote branches
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
type RemoteLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getGoGitRemotes func() ([]*gogit.Remote, error)
}
func NewRemoteLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
getGoGitRemotes func() ([]*gogit.Remote, error),
) *RemoteLoader {
return &RemoteLoader{
Common: common,
cmd: cmd,
getGoGitRemotes: getGoGitRemotes,
}
}
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
remoteBranchesStr, err := self.cmd.New("git branch -r").RunWithOutput()
if err != nil {
return nil, err
}
goGitRemotes, err := c.Repo.Remotes()
goGitRemotes, err := self.getGoGitRemotes()
if err != nil {
return nil, err
}
@@ -27,7 +46,7 @@ func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
for i, goGitRemote := range goGitRemotes {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*models.RemoteBranch, len(matches))
for j, match := range matches {
+80
View File
@@ -0,0 +1,80 @@
package loaders
import (
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type StashLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewStashLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *StashLoader {
return &StashLoader{
Common: common,
cmd: cmd,
}
}
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return self.getUnfilteredStashEntries()
}
rawString, err := self.cmd.New("git stash list --name-only").RunWithOutput()
if err != nil {
return self.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return self.getUnfilteredStashEntries()
}
currentStashEntry = self.stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").RunWithOutput()
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, self.stashEntryFromLine(line, i))
}
return stashEntries
}
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}
+59
View File
@@ -0,0 +1,59 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
filterPath string
runner oscommands.ICmdObjRunner
expectedStashEntries []*models.StashEntry
}
scenarios := []scenario{
{
"No stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(`git stash list --pretty='%gs'`, "", nil),
[]*models.StashEntry{},
},
{
"Several stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(
`git stash list --pretty='%gs'`,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template",
nil,
),
[]*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(""))
})
}
}
@@ -1,16 +1,33 @@
package commands
package loaders
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
type TagLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewTagLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *TagLoader {
return &TagLoader{
Common: common,
cmd: cmd,
}
}
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list --sort=-creatordate`)
remoteBranchesStr, err := self.cmd.New(`git tag --list --sort=-creatordate`).RunWithOutput()
if err != nil {
return nil, err
}
-42
View File
@@ -1,42 +0,0 @@
package commands
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
// GetFilesInDiff get the specified commit files
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
if err != nil {
return nil, err
}
return c.getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// typical result looks like 'A my_file' meaning my_file was added
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Name: name,
ChangeStatus: changeStatus,
})
}
return commitFiles
}
-114
View File
@@ -1,114 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := oscommands.NewDummyOSCommand()
return &CommitListBuilder{
Log: utils.NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), "auto"),
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return secureexec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return secureexec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return secureexec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase("HEAD"))
})
}
}
-227
View File
@@ -1,227 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetStatusFiles is a function.
func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.File)
}
scenarios := []scenario{
{
"No files found",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
func(files []*models.File) {
assert.Len(t, files, 0)
},
},
{
"Several files found",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`MM file1.txt\0A file3.txt\0AM file2.txt\0?? file4.txt\0UU file5.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 5)
expected := []*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "other",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "other",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "other",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "other",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "other",
ShortStatus: "UU",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"File with new line char",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`MM a\nb.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 1)
expected := []*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "other",
ShortStatus: "MM",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"Renamed files",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`R after1.txt\0before1.txt\0RM after2.txt\0before2.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 2)
expected := []*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "other",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "other",
ShortStatus: "RM",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"File with arrow in name",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`?? a -> b.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 1)
expected := []*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "other",
ShortStatus: "??",
},
}
assert.EqualValues(t, expected, files)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
})
}
}
-65
View File
@@ -1,65 +0,0 @@
package commands
import (
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) getUnfilteredStashEntries() []*models.StashEntry {
unescaped := "git stash list --pretty='%gs'"
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return c.getUnfilteredStashEntries()
}
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
if err != nil {
return c.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return c.getUnfilteredStashEntries()
}
currentStashEntry = stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func stashEntryFromLine(line string, index int) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}
-61
View File
@@ -1,61 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetStashEntries is a function.
func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.StashEntry)
}
scenarios := []scenario{
{
"No stash entries found",
func(string, ...string) *exec.Cmd {
return secureexec.Command("echo")
},
func(entries []*models.StashEntry) {
assert.Len(t, entries, 0)
},
},
{
"Several stash entries found",
func(string, ...string) *exec.Cmd {
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
},
func(entries []*models.StashEntry) {
expected := []*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
}
assert.Len(t, entries, 2)
assert.EqualValues(t, expected, entries)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetStashEntries(""))
})
}
}
+40 -2
View File
@@ -5,17 +5,33 @@ import (
)
// A command object is a general way to represent a command to be run on the
// command line. If you want to log the command you'll use .ToString() and
// if you want to run it you'll use .GetCmd()
// command line.
type ICmdObj interface {
GetCmd() *exec.Cmd
// outputs string representation of command. Note that if the command was built
// using NewFromArgs, the output won't be quite the same as what you would type
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
ToString() string
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
// runs the command and returns an error if any
Run() error
// runs the command and returns the output as a string, and an error if any
RunWithOutput() (string, error)
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
RunAndProcessLines(onLine func(line string) (bool, error)) error
// logs command
Log() ICmdObj
}
type CmdObj struct {
cmdStr string
cmd *exec.Cmd
runner ICmdObjRunner
logCommand func(ICmdObj)
}
func (self *CmdObj) GetCmd() *exec.Cmd {
@@ -31,3 +47,25 @@ func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
return self
}
func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env
}
func (self *CmdObj) Log() ICmdObj {
self.logCommand(self)
return self
}
func (self *CmdObj) Run() error {
return self.runner.Run(self)
}
func (self *CmdObj) RunWithOutput() (string, error) {
return self.runner.RunWithOutput(self)
}
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}
@@ -0,0 +1,68 @@
package oscommands
import (
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/mgutz/str"
)
type ICmdObjBuilder interface {
// New returns a new command object based on the string provided
New(cmdStr string) ICmdObj
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
NewShell(commandStr string) ICmdObj
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object. This can be useful when you don't want to worry about whitespace and quoting and stuff.
NewFromArgs(args []string) ICmdObj
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
Quote(str string) string
}
type CmdObjBuilder struct {
runner ICmdObjRunner
logCmdObj func(ICmdObj)
platform *Platform
}
// poor man's version of explicitly saying that struct X implements interface Y
var _ ICmdObjBuilder = &CmdObjBuilder{}
func (self *CmdObjBuilder) New(cmdStr string) ICmdObj {
args := str.ToArgv(cmdStr)
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: cmdStr,
cmd: cmd,
runner: self.runner,
logCommand: self.logCmdObj,
}
}
func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: strings.Join(args, " "),
cmd: cmd,
runner: self.runner,
logCommand: self.logCmdObj,
}
}
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
return self.NewFromArgs([]string{self.platform.Shell, self.platform.ShellArg, commandStr})
}
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {
decoratedRunner := decorate(self.runner)
return &CmdObjBuilder{
runner: decoratedRunner,
logCmdObj: self.logCmdObj,
platform: self.platform,
}
}
+79
View File
@@ -0,0 +1,79 @@
package oscommands
import (
"bufio"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type ICmdObjRunner interface {
Run(cmdObj ICmdObj) error
RunWithOutput(cmdObj ICmdObj) (string, error)
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
}
type cmdObjRunner struct {
log *logrus.Entry
logCmdObj func(ICmdObj)
}
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
self.logCmdObj(cmdObj)
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(output)
}
return output, err
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
cmd := cmdObj.GetCmd()
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
_ = cmd.Process.Kill()
break
}
}
_ = cmd.Wait()
return nil
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
+22 -2
View File
@@ -1,11 +1,31 @@
package oscommands
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(utils.NewDummyLog(), config.NewDummyAppConfig())
return NewOSCommand(utils.NewDummyCommon())
}
func NewDummyCmdObjBuilder(runner ICmdObjRunner) *CmdObjBuilder {
return &CmdObjBuilder{
runner: runner,
logCmdObj: func(ICmdObj) {},
platform: &Platform{
OS: "darwin",
Shell: "bash",
ShellArg: "-c",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
},
}
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon())
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand
}
@@ -0,0 +1,102 @@
package oscommands
import (
"bufio"
"fmt"
"strings"
"testing"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
// for use in testing
type FakeCmdObjRunner struct {
t *testing.T
expectedCmds []func(ICmdObj) (string, error)
expectedCmdIndex int
}
var _ ICmdObjRunner = &FakeCmdObjRunner{}
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner {
return &FakeCmdObjRunner{t: t}
}
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if self.expectedCmdIndex > len(self.expectedCmds)-1 {
self.t.Errorf("ran too many commands. Unexpected command: `%s`", cmdObj.ToString())
return "", errors.New("ran too many commands")
}
expectedCmd := self.expectedCmds[self.expectedCmdIndex]
output, err := expectedCmd(cmdObj)
self.expectedCmdIndex++
return output, err
}
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
output, err := self.RunWithOutput(cmdObj)
if err != nil {
return err
}
scanner := bufio.NewScanner(strings.NewReader(output))
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
break
}
}
return nil
}
func (self *FakeCmdObjRunner) ExpectFunc(fn func(cmdObj ICmdObj) (string, error)) *FakeCmdObjRunner {
self.expectedCmds = append(self.expectedCmds, fn)
return self
}
func (self *FakeCmdObjRunner) Expect(expectedCmdStr string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
cmdStr := cmdObj.ToString()
assert.Equal(self.t, expectedCmdStr, cmdStr, fmt.Sprintf("expected command %d to be %s, but was %s", self.expectedCmdIndex+1, expectedCmdStr, cmdStr))
return output, err
})
return self
}
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
args := cmdObj.GetCmd().Args
assert.EqualValues(self.t, expectedArgs, args, fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
return output, err
})
return self
}
func (self *FakeCmdObjRunner) CheckForMissingCalls() {
if self.expectedCmdIndex < len(self.expectedCmds) {
self.t.Errorf("expected command %d to be called, but was not", self.expectedCmdIndex+1)
}
return
}
+45 -249
View File
@@ -1,7 +1,6 @@
package oscommands
import (
"bufio"
"fmt"
"io/ioutil"
"os"
@@ -13,30 +12,15 @@ import (
"github.com/go-errors/errors"
"github.com/atotto/clipboard"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
)
// Platform stores the os state
type Platform struct {
OS string
Shell string
ShellArg string
OpenCommand string
OpenLinkCommand string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
Command func(string, ...string) *exec.Cmd
BeforeExecuteCmd func(*exec.Cmd)
Getenv func(string) string
*common.Common
Platform *Platform
Getenv func(string) string
// callback to run before running a command, i.e. for the purposes of logging
onRunCommand func(CmdLogEntry)
@@ -45,6 +29,17 @@ type OSCommand struct {
CmdLogSpan string
removeFile func(string) error
Cmd *CmdObjBuilder
}
// Platform stores the os state
type Platform struct {
OS string
Shell string
ShellArg string
OpenCommand string
OpenLinkCommand string
}
// TODO: make these fields private
@@ -78,16 +73,20 @@ func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
Command: secureexec.Command,
BeforeExecuteCmd: func(*exec.Cmd) {},
Getenv: os.Getenv,
removeFile: os.RemoveAll,
func NewOSCommand(common *common.Common) *OSCommand {
platform := getPlatform()
c := &OSCommand{
Common: common,
Platform: platform,
Getenv: os.Getenv,
removeFile: os.RemoveAll,
}
runner := &cmdObjRunner{log: common.Log, logCmdObj: c.LogCmdObj}
c.Cmd = &CmdObjBuilder{runner: runner, logCmdObj: c.LogCmdObj, platform: platform}
return c
}
func (c *OSCommand) WithSpan(span string) *OSCommand {
@@ -101,11 +100,13 @@ func (c *OSCommand) WithSpan(span string) *OSCommand {
newOSCommand := &OSCommand{}
*newOSCommand = *c
newOSCommand.CmdLogSpan = span
newOSCommand.Cmd.logCmdObj = newOSCommand.LogCmdObj
newOSCommand.Cmd.runner = &cmdObjRunner{log: c.Log, logCmdObj: newOSCommand.LogCmdObj}
return newOSCommand
}
func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) {
c.LogCommand(strings.Join(cmd.Args, " "), true)
func (c *OSCommand) LogCmdObj(cmdObj ICmdObj) {
c.LogCommand(cmdObj.ToString(), true)
}
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
@@ -120,121 +121,13 @@ func (c *OSCommand) SetOnRunCommand(f func(CmdLogEntry)) {
c.onRunCommand = f
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.Command = cmd
}
// To be used for testing only
func (c *OSCommand) SetRemoveFile(f func(string) error) {
c.removeFile = f
}
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.BeforeExecuteCmd = cmd
}
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.LogCommand(command, true)
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
}
// RunCommandWithOutput wrapper around commands returning their output and error
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
// with a percent sign because it thinks it's supposed to be a formatString when
// in that case it's not. To get around that error you'll need to define the string
// in a variable and pass the variable into RunCommandWithOutput.
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
command := formatString
if formatArgs != nil {
command = fmt.Sprintf(formatString, formatArgs...)
}
cmd := c.ExecutableFromString(command)
c.LogExecCmd(cmd)
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
c.Log.WithField("command", command).Error(output)
}
return output, err
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
c.LogExecCmd(cmd)
c.BeforeExecuteCmd(cmd)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
cmd := c.Command(splitCmd[0], splitCmd[1:]...)
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
return cmd
}
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
quotedCommand := ""
// Windows does not seem to like quotes around the command
if c.Platform.OS == "windows" {
quotedCommand = strings.NewReplacer(
"^", "^^",
"&", "^&",
"|", "^|",
"<", "^<",
">", "^>",
"%", "^%",
).Replace(commandStr)
} else {
quotedCommand = c.Quote(commandStr)
}
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
return c.ExecutableFromString(shellCommand)
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
return err
}
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
// need access to the shell
func (c *OSCommand) RunShellCommand(command string) error {
cmd := c.ShellCommandFromString(command)
c.LogExecCmd(cmd)
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
return err
}
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
func FileType(path string) string {
fileInfo, err := os.Stat(path)
if err != nil {
return "other"
@@ -245,63 +138,36 @@ func (c *OSCommand) FileType(path string) string {
return "file"
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().OS.OpenCommand
commandTemplate := c.UserConfig.OS.OpenCommand
templateValues := map[string]string{
"filename": c.Quote(filename),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunShellCommand(command)
return err
return c.Cmd.NewShell(command).Run()
}
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false)
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
commandTemplate := c.UserConfig.OS.OpenLinkCommand
templateValues := map[string]string{
"link": c.Quote(link),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunShellCommand(command)
return err
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.Command(cmdName, commandArgs...)
if cmd != nil {
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
}
c.LogExecCmd(cmd)
return cmd
}
// PrepareShellSubProcess returns the pointer to a custom command
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
return c.Cmd.NewShell(command).Run()
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
return c.Cmd.Quote(message)
}
func (self *CmdObjBuilder) Quote(message string) string {
var quote string
if c.Platform.OS == "windows" {
if self.platform.OS == "windows" {
quote = `\"`
message = strings.NewReplacer(
`"`, `"'"'"`,
@@ -390,24 +256,6 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
c.BeforeExecuteCmd(cmd)
c.LogExecCmd(cmd)
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
@@ -426,7 +274,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
logCmdStr += " | "
}
logCmdStr += str
cmds[i] = c.ExecutableFromString(str)
cmds[i] = c.Cmd.New(str).GetCmd()
}
c.LogCommand(logCmdStr, true)
@@ -489,35 +337,6 @@ func Kill(cmd *exec.Cmd) error {
return cmd.Process.Kill()
}
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
_ = cmd.Process.Kill()
break
}
}
_ = cmd.Wait()
return nil
}
func (c *OSCommand) CopyToClipboard(str string) error {
escaped := strings.Replace(str, "\n", "\\n", -1)
truncated := utils.TruncateWithEllipsis(escaped, 40)
@@ -531,29 +350,6 @@ func (c *OSCommand) RemoveFile(path string) error {
return c.removeFile(path)
}
func (c *OSCommand) NewCmdObjFromStr(cmdStr string) ICmdObj {
args := str.ToArgv(cmdStr)
cmd := c.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: cmdStr,
cmd: cmd,
}
}
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
cmd := c.Command(args[0], args[1:]...)
return &CmdObj{
cmdStr: strings.Join(args, " "),
cmd: cmd,
}
}
func (c *OSCommand) NewCmdObj(cmd *exec.Cmd) ICmdObj {
return &CmdObj{
cmdStr: strings.Join(cmd.Args, " "),
cmd: cmd,
}
func GetTempDir() string {
return filepath.Join(os.TempDir(), "lazygit")
}
+109 -65
View File
@@ -7,59 +7,50 @@ import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
// TestOSCommandOpenFileDarwin is a function.
func TestOSCommandOpenFileDarwin(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
runner *FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `open "test"`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `open "filename with spaces"`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `open "filename with spaces"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Platform.OS = "darwin"
OSCmd.Command = s.command
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "darwin"
oSCmd.UserConfig.OS.OpenCommand = "open {{filename}}"
s.test(OSCmd.OpenFile(s.filename))
s.test(oSCmd.OpenFile(s.filename))
}
}
@@ -67,72 +58,125 @@ func TestOSCommandOpenFileDarwin(t *testing.T) {
func TestOSCommandOpenFileLinux(t *testing.T) {
type scenario struct {
filename string
runner *FakeCmdObjRunner
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "test" > /dev/null`}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "test" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "test" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "filename with spaces" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "filename with spaces" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"let's_test_with_single_quote",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"$USER.txt",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "\$USER.txt" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "\$USER.txt" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Platform.OS = "linux"
OSCmd.Config.GetUserConfig().OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "linux"
oSCmd.UserConfig.OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
s.test(OSCmd.OpenFile(s.filename))
s.test(oSCmd.OpenFile(s.filename))
}
}
func TestOSCommandOpenFileWindows(t *testing.T) {
type scenario struct {
filename string
runner *FakeCmdObjRunner
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
oSCmd := NewDummyOSCommandWithRunner(s.runner)
platform := &Platform{
OS: "windows",
Shell: "cmd",
ShellArg: "/c",
}
oSCmd.Platform = platform
oSCmd.Cmd.platform = platform
oSCmd.UserConfig.OS.OpenCommand = `start "" {{filename}}`
s.test(oSCmd.OpenFile(s.filename))
}
}
+7 -9
View File
@@ -8,8 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
func TestOSCommandRunWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
@@ -32,12 +31,12 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
c := NewDummyOSCommand()
s.test(c.Cmd.New(s.command).RunWithOutput())
}
}
// TestOSCommandRunCommand is a function.
func TestOSCommandRunCommand(t *testing.T) {
func TestOSCommandRun(t *testing.T) {
type scenario struct {
command string
test func(error)
@@ -53,11 +52,11 @@ func TestOSCommandRunCommand(t *testing.T) {
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommand(s.command))
c := NewDummyOSCommand()
s.test(c.Cmd.New(s.command).Run())
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
@@ -109,7 +108,6 @@ func TestOSCommandQuoteWindows(t *testing.T) {
assert.EqualValues(t, expected, actual)
}
// TestOSCommandFileType is a function.
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
@@ -162,7 +160,7 @@ func TestOSCommandFileType(t *testing.T) {
for _, s := range scenarios {
s.setup()
s.test(NewDummyOSCommand().FileType(s.path))
s.test(FileType(s.path))
_ = os.RemoveAll(s.path)
}
}
+31 -39
View File
@@ -11,76 +11,68 @@ import (
"github.com/stretchr/testify/assert"
)
// TestOSCommandOpenFileWindows tests the OpenFile command on Linux
func TestOSCommandOpenFileWindows(t *testing.T) {
type scenario struct {
filename string
runner *FakeCmdObjRunner
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "test"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "filename with spaces"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"let's_test_with_single_quote",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "let's_test_with_single_quote"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"$USER.txt",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "$USER.txt"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Platform.OS = "windows"
OSCmd.Config.GetUserConfig().OS.OpenCommand = `start "" {{filename}}`
oSCmd := NewDummyOSCommandWithRunner(s.runner)
platform := &Platform{
OS: "windows",
Shell: "cmd",
ShellArg: "/c",
}
oSCmd.Platform = platform
oSCmd.Cmd.platform = platform
oSCmd.UserConfig.OS.OpenCommand = `start "" {{filename}}`
s.test(OSCmd.OpenFile(s.filename))
s.test(oSCmd.OpenFile(s.filename))
}
}
+6 -5
View File
@@ -6,6 +6,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
// DeletePatchesFromCommit applies a patch in reverse for a commit
@@ -85,12 +86,12 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
if err := cmdObj.Run(); err != nil {
return err
}
@@ -149,7 +150,7 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
}
if err := p.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
@@ -169,7 +170,7 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
@@ -217,7 +218,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
head_message, _ := c.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
err := c.OSCommand.RunCommand(c.CommitCmdStr(new_message, ""))
err := c.CommitCmdObj(new_message, "").Run()
if err != nil {
return err
}
-186
View File
@@ -1,186 +0,0 @@
package commands
import (
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
)
// Service is a service that repository is on (Github, Bitbucket, ...)
type Service struct {
Name string
pullRequestURLIntoDefaultBranch func(owner string, repository string, from string) string
pullRequestURLIntoTargetBranch func(owner string, repository string, from string, to string) string
}
// NewService builds a Service based on the host type
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
var service *Service
switch typeName {
case "github":
service = &Service{
Name: repositoryDomain,
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
return fmt.Sprintf("https://%s/%s/%s/compare/%s?expand=1", siteDomain, owner, repository, from)
},
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
return fmt.Sprintf("https://%s/%s/%s/compare/%s...%s?expand=1", siteDomain, owner, repository, to, from)
},
}
case "bitbucket":
service = &Service{
Name: repositoryDomain,
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&t=1", siteDomain, owner, repository, from)
},
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&dest=%s&t=1", siteDomain, owner, repository, from, to)
},
}
case "gitlab":
service = &Service{
Name: repositoryDomain,
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s", siteDomain, owner, repository, from)
},
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s&merge_request[target_branch]=%s", siteDomain, owner, repository, from, to)
},
}
}
return service
}
func (s *Service) PullRequestURL(owner string, repository string, from string, to string) string {
if to == "" {
return s.pullRequestURLIntoDefaultBranch(owner, repository, from)
} else {
return s.pullRequestURLIntoTargetBranch(owner, repository, from, to)
}
}
// PullRequest opens a link in browser to create new pull request
// with selected branch
type PullRequest struct {
GitServices []*Service
GitCommand *GitCommand
}
// RepoInformation holds some basic information about the repo
type RepoInformation struct {
Owner string
Repository string
}
func getServices(config config.AppConfigurer) []*Service {
services := []*Service{
NewService("github", "github.com", "github.com"),
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
NewService("gitlab", "gitlab.com", "gitlab.com"),
}
configServices := config.GetUserConfig().Services
for repoDomain, typeAndDomain := range configServices {
splitData := strings.Split(typeAndDomain, ":")
if len(splitData) != 2 {
// TODO log this misconfiguration
continue
}
service := NewService(splitData[0], repoDomain, splitData[1])
if service == nil {
// TODO log this unsupported service
continue
}
services = append(services, service)
}
return services
}
// NewPullRequest creates new instance of PullRequest
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
return &PullRequest{
GitServices: getServices(gitCommand.Config),
GitCommand: gitCommand,
}
}
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(from string, to string) (string, error) {
pullRequestURL, err := pr.getPullRequestURL(from, to)
if err != nil {
return "", err
}
return pullRequestURL, pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
}
// CopyURL copies the pull request URL to the clipboard
func (pr *PullRequest) CopyURL(from string, to string) (string, error) {
pullRequestURL, err := pr.getPullRequestURL(from, to)
if err != nil {
return "", err
}
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
}
func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error) {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(from)
if !branchExistsOnRemote {
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
}
repoURL := pr.GitCommand.GetRemoteURL()
var gitService *Service
for _, service := range pr.GitServices {
if strings.Contains(repoURL, service.Name) {
gitService = service
break
}
}
if gitService == nil {
return "", errors.New(pr.GitCommand.Tr.UnsupportedGitService)
}
repoInfo := getRepoInfoFromURL(repoURL)
pullRequestURL := gitService.PullRequestURL(repoInfo.Owner, repoInfo.Repository, from, to)
return pullRequestURL, nil
}
func getRepoInfoFromURL(url string) *RepoInformation {
isHTTP := strings.HasPrefix(url, "http")
if isHTTP {
splits := strings.Split(url, "/")
owner := strings.Join(splits[3:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := strings.Join(splits[0:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
-256
View File
@@ -1,256 +0,0 @@
//go:build !windows
// +build !windows
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestCreatePullRequest is a function.
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
from string
to string
remoteUrl string
command func(string, ...string) *exec.Cmd
test func(url string, err error)
}
scenarios := []scenario{
{
testName: "Opens a link to new pull request on bitbucket",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url",
from: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github",
from: "feature/sum-operation",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with specific target branch",
from: "feature/profile-page/avatar",
to: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
from: "feature/remote-events",
to: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on gitlab",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab in nested groups",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "bash")
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
remoteUrl: "git@something.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.Command = s.command
gitCommand.OSCommand.Platform.OS = "darwin"
gitCommand.OSCommand.Platform.Shell = "bash"
gitCommand.OSCommand.Platform.ShellArg = "-c"
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.from, s.to))
})
}
}
-41
View File
@@ -1,41 +0,0 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestGetRepoInfoFromURL is a function.
func TestGetRepoInfoFromURL(t *testing.T) {
type scenario struct {
testName string
repoURL string
test func(*RepoInformation)
}
scenarios := []scenario{
{
"Returns repository information for git remote url",
"git@github.com:petersmith/super_calculator",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "petersmith")
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
},
},
{
"Returns repository information for http remote url",
"https://my_username@bitbucket.org/johndoe/social_network.git",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "johndoe")
assert.EqualValues(t, repoInfo.Repository, "social_network")
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(getRepoInfoFromURL(s.repoURL))
})
}
}
-256
View File
@@ -1,256 +0,0 @@
//go:build windows
// +build windows
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestCreatePullRequestOnWindows is a function.
func TestCreatePullRequestOnWindows(t *testing.T) {
type scenario struct {
testName string
from string
to string
remoteUrl string
command func(string, ...string) *exec.Cmd
test func(url string, err error)
}
scenarios := []scenario{
{
testName: "Opens a link to new pull request on bitbucket",
from: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url",
from: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github",
from: "feature/sum-operation",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with specific target branch",
from: "feature/profile-page/avatar",
to: "feature/profile-page",
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar^&dest=feature/profile-page^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
},
},
{
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
from: "feature/remote-events",
to: "feature/events",
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events^&dest=feature/events^&t=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
},
},
{
testName: "Opens a link to new pull request on github with specific target branch",
from: "feature/sum-operation",
to: "feature/operations",
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
},
},
{
testName: "Opens a link to new pull request on gitlab",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab in nested groups",
from: "feature/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
from: "feature/commit-ui",
to: "epic/ui",
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "cmd")
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.NoError(t, err)
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
},
},
{
testName: "Throws an error if git service is unsupported",
from: "feature/divide-operation",
remoteUrl: "git@something.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
test: func(url string, err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.Command = s.command
gitCommand.OSCommand.Platform.OS = "windows"
gitCommand.OSCommand.Platform.Shell = "cmd"
gitCommand.OSCommand.Platform.ShellArg = "/c"
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = `start "" {{link}}`
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.from, s.to))
})
}
}
+25 -34
View File
@@ -3,17 +3,15 @@ package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/mgutz/str"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) {
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
if err != nil {
return nil, err
@@ -35,12 +33,12 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error {
@@ -49,30 +47,29 @@ func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, acti
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (oscommands.ICmdObj, error) {
ex := c.OSCommand.GetLazygitPath()
debug := "FALSE"
if c.OSCommand.Config.GetDebug() {
if c.Debug {
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
c.Log.WithField("command", cmdStr).Info("RunCommand")
splitCmd := str.ToArgv(cmdStr)
cmd := c.OSCommand.Command(splitCmd[0], splitCmd[1:]...)
cmdObj := c.Cmd.New(cmdStr)
gitSequenceEditor := ex
if todo == "" {
@@ -81,9 +78,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
}
cmd.Env = os.Environ()
cmd.Env = append(
cmd.Env,
cmdObj.AddEnvVars(
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
"LAZYGIT_REBASE_TODO="+todo,
"DEBUG="+debug,
@@ -93,10 +88,10 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
)
if overrideEditor {
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
}
return cmd, nil
return cmdObj, nil
}
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
@@ -222,26 +217,22 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
return nil
return cmdObj.Run()
}
// RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error {
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
cmdObj, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
@@ -276,14 +267,14 @@ func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command stri
}
func (c *GitCommand) runSkipEditorCommand(command string) error {
cmd := c.OSCommand.ExecutableFromString(command)
cmdObj := c.OSCommand.Cmd.New(command)
lazyGitPath := c.OSCommand.GetLazygitPath()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
)
return c.OSCommand.RunExecutable(cmd)
return cmdObj.
AddEnvVars(
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
).
Run()
}
+37 -57
View File
@@ -1,57 +1,47 @@
package commands
import (
"os/exec"
"regexp"
"testing"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// TestGitCommandRebaseBranch is a function.
func TestGitCommandRebaseBranch(t *testing.T) {
type scenario struct {
testName string
arg string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"successful rebase",
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty master",
Replace: "echo",
},
}),
func(err error) {
testName: "successful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"unsuccessful rebase",
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty master",
Replace: "test",
},
}),
func(err error) {
testName: "unsuccessful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.RebaseBranch(s.arg))
})
}
@@ -60,37 +50,27 @@ func TestGitCommandRebaseBranch(t *testing.T) {
// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects
// environment variables that suppress an interactive editor
func TestGitCommandSkipEditorCommand(t *testing.T) {
cmd := NewDummyGitCommand()
cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) {
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^VISUAL="),
"expected VISUAL to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^EDITOR="),
"expected EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^GIT_EDITOR="),
"expected GIT_EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"),
"expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command",
)
commandStr := "git blah"
runner := oscommands.NewFakeRunner(t).ExpectFunc(func(cmdObj oscommands.ICmdObj) (string, error) {
assert.Equal(t, commandStr, cmdObj.ToString())
envVars := cmdObj.GetEnvVars()
for _, regexStr := range []string{
`^VISUAL=.*$`,
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
"^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$",
} {
foundMatch := utils.IncludesStringFunc(envVars, func(envVar string) bool {
return regexp.MustCompile(regexStr).MatchString(envVar)
})
if !foundMatch {
t.Errorf("expected environment variable %s to be set", regexStr)
}
}
return "", nil
})
_ = cmd.runSkipEditorCommand("true")
gitCmd := NewDummyGitCommandWithRunner(runner)
err := gitCmd.runSkipEditorCommand(commandStr)
assert.NoError(t, err)
runner.CheckForMissingCalls()
}
+21 -10
View File
@@ -7,24 +7,33 @@ import (
)
func (c *GitCommand) AddRemote(name string, url string) error {
return c.RunCommand("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(url))
return c.Cmd.
New(fmt.Sprintf("git remote add %s %s", c.Cmd.Quote(name), c.Cmd.Quote(url))).
Run()
}
func (c *GitCommand) RemoveRemote(name string) error {
return c.RunCommand("git remote remove %s", c.OSCommand.Quote(name))
return c.Cmd.
New(fmt.Sprintf("git remote remove %s", c.Cmd.Quote(name))).
Run()
}
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.RunCommand("git remote rename %s %s", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName))
return c.Cmd.
New(fmt.Sprintf("git remote rename %s %s", c.Cmd.Quote(oldRemoteName), c.Cmd.Quote(newRemoteName))).
Run()
}
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.RunCommand("git remote set-url %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(updatedUrl))
return c.Cmd.
New(fmt.Sprintf("git remote set-url %s %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(updatedUrl))).
Run()
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(command)
command := fmt.Sprintf("git push %s --delete %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(branchName))
cmdObj := c.Cmd.
New(command)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
@@ -34,10 +43,12 @@ func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCre
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
_, err := c.OSCommand.RunCommandWithOutput(
"git show-ref --verify -- refs/remotes/origin/%s",
c.OSCommand.Quote(branchName),
)
_, err := c.Cmd.
New(
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
c.Cmd.Quote(branchName),
)).
RunWithOutput()
return err == nil
}
+15 -8
View File
@@ -1,28 +1,32 @@
package commands
import "fmt"
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
)
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.RunCommand("git stash %s stash@{%d}", method, index)
return c.Cmd.New(fmt.Sprintf("git stash %s stash@{%d}", method, index)).Run()
}
// StashSave save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.RunCommand("git stash save %s", c.OSCommand.Quote(message))
return c.Cmd.New("git stash save " + c.OSCommand.Quote(message)).Run()
}
// GetStashEntryDiff stash diff
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
return fmt.Sprintf("git stash show -p --stat --color=%s stash@{%d}", c.colorArg(), index)
return fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", c.colorArg(), c.UserConfig.Git.DiffContextSize, index)
}
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
func (c *GitCommand) StashSaveStagedChanges(message string) error {
// wrap in 'writing', which uses a mutex
if err := c.RunCommand("git stash --keep-index"); err != nil {
if err := c.Cmd.New("git stash --keep-index").Run(); err != nil {
return err
}
@@ -30,7 +34,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
return err
}
if err := c.RunCommand("git stash apply stash@{1}"); err != nil {
if err := c.Cmd.New("git stash apply stash@{1}").Run(); err != nil {
return err
}
@@ -38,14 +42,17 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
return err
}
if err := c.RunCommand("git stash drop stash@{1}"); err != nil {
if err := c.Cmd.New("git stash drop stash@{1}").Run(); err != nil {
return err
}
// if you had staged an untracked file, that will now appear as 'AD' in git status
// meaning it's deleted in your working tree but added in your index. Given that it's
// now safely stashed, we need to remove it.
files := c.GetStatusFiles(GetStatusFileOptions{})
files := loaders.
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
if err := c.UnStageFile(file.Names(), false); err != nil {
+42 -18
View File
@@ -1,35 +1,59 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
// TestGitCommandStashDo is a function.
func TestGitCommandStashDo(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "stash", "drop", "stash@{1}"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StashDo(1, "drop"))
runner.CheckForMissingCalls()
}
// TestGitCommandStashSave is a function.
func TestGitCommandStashSave(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "stash", "save", "A stash message"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StashSave("A stash message"))
runner.CheckForMissingCalls()
}
func TestGitCommandShowStashEntryCmdStr(t *testing.T) {
type scenario struct {
testName string
index int
contextSize int
expected string
}
scenarios := []scenario{
{
testName: "Default case",
index: 5,
contextSize: 3,
expected: "git stash show -p --stat --color=always --unified=3 stash@{5}",
},
{
testName: "Show diff with custom context size",
index: 5,
contextSize: 77,
expected: "git stash show -p --stat --color=always --unified=77 stash@{5}",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
cmdStr := gitCmd.ShowStashEntryCmdStr(s.index)
assert.Equal(t, s.expected, cmdStr)
})
}
}
+11 -17
View File
@@ -4,43 +4,37 @@ import (
"path/filepath"
gogit "github.com/jesseduffield/go-git/v5"
)
const (
REBASE_MODE_NORMAL = "normal"
REBASE_MODE_INTERACTIVE = "interactive"
REBASE_MODE_REBASING = "rebasing"
REBASE_MODE_MERGING = "merging"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) {
exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply"))
if err != nil {
return "", err
return enums.REBASE_MODE_NONE, err
}
if exists {
return REBASE_MODE_NORMAL, nil
return enums.REBASE_MODE_NORMAL, nil
}
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
if exists {
return REBASE_MODE_INTERACTIVE, err
return enums.REBASE_MODE_INTERACTIVE, err
} else {
return "", err
return enums.REBASE_MODE_NONE, err
}
}
func (c *GitCommand) WorkingTreeState() string {
func (c *GitCommand) WorkingTreeState() enums.RebaseMode {
rebaseMode, _ := c.RebaseMode()
if rebaseMode != "" {
return REBASE_MODE_REBASING
if rebaseMode != enums.REBASE_MODE_NONE {
return enums.REBASE_MODE_REBASING
}
merging, _ := c.IsInMergeState()
if merging {
return REBASE_MODE_MERGING
return enums.REBASE_MODE_MERGING
}
return REBASE_MODE_NORMAL
return enums.REBASE_MODE_NONE
}
// IsInMergeState states whether we are still mid-merge
+61 -40
View File
@@ -2,12 +2,15 @@ package commands
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
// .gitmodules looks like this:
@@ -15,7 +18,22 @@ import (
// path = blah/mysubmodule
// url = git@github.com:subbo.git
func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
type SubmoduleCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
dotGitDir string
}
func NewSubmoduleCommands(common *common.Common, cmd oscommands.ICmdObjBuilder, dotGitDir string) SubmoduleCommands {
return SubmoduleCommands{
Common: common,
cmd: cmd,
dotGitDir: dotGitDir,
}
}
func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) {
file, err := os.Open(".gitmodules")
if err != nil {
if os.IsNotExist(err) {
@@ -61,36 +79,36 @@ func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
return configs, nil
}
func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
c.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
self.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
return nil
}
return c.RunCommand("git -C %s stash --include-untracked", c.OSCommand.Quote(submodule.Path))
return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run()
}
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
return c.RunCommand("git submodule update --init --force -- %s", c.OSCommand.Quote(submodule.Path))
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run()
}
func (c *GitCommand) SubmoduleUpdateAll() error {
func (self *SubmoduleCommands) UpdateAll() error {
// not doing an --init here because the user probably doesn't want that
return c.RunCommand("git submodule update --force")
return self.cmd.New("git submodule update --force").Run()
}
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := c.RunCommand("git submodule deinit --force -- %s", c.OSCommand.Quote(submodule.Path)); err != nil {
if err := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); err != nil {
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
if err := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
if err := c.RunCommand("git config --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
@@ -100,66 +118,69 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
}
}
if err := c.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
if err := self.cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil {
// if the directory isn't there then that's fine
c.Log.Error(err)
self.Log.Error(err)
}
return os.RemoveAll(filepath.Join(c.DotGitDir, "modules", submodule.Path))
return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path))
}
func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
return c.OSCommand.RunCommand(
"git submodule add --force --name %s -- %s %s ",
c.OSCommand.Quote(name),
c.OSCommand.Quote(url),
c.OSCommand.Quote(path),
)
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
return self.cmd.
New(
fmt.Sprintf(
"git submodule add --force --name %s -- %s %s ",
self.cmd.Quote(name),
self.cmd.Quote(url),
self.cmd.Quote(path),
)).
Run()
}
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error {
// the set-url command is only for later git versions so we're doing it manually here
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", c.OSCommand.Quote(name), c.OSCommand.Quote(newUrl)); err != nil {
if err := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil {
return err
}
if err := c.RunCommand("git submodule sync -- %s", c.OSCommand.Quote(path)); err != nil {
if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil {
return err
}
return nil
}
func (c *GitCommand) SubmoduleInit(path string) error {
return c.RunCommand("git submodule init -- %s", c.OSCommand.Quote(path))
func (self *SubmoduleCommands) Init(path string) error {
return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run()
}
func (c *GitCommand) SubmoduleUpdate(path string) error {
return c.RunCommand("git submodule update --init -- %s", c.OSCommand.Quote(path))
func (self *SubmoduleCommands) Update(path string) error {
return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run()
}
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
return "git submodule init"
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule init")
}
func (c *GitCommand) SubmoduleBulkUpdateCmdStr() string {
return "git submodule update"
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update")
}
func (c *GitCommand) SubmoduleForceBulkUpdateCmdStr() string {
return "git submodule update --force"
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update --force")
}
func (c *GitCommand) SubmoduleBulkDeinitCmdStr() string {
return "git submodule deinit --all --force"
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule deinit --all --force")
}
func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
for _, submodule := range submodules {
if err := c.SubmoduleStash(submodule); err != nil {
if err := self.Stash(submodule); err != nil {
return err
}
}
return c.SubmoduleUpdateAll()
return self.UpdateAll()
}
+22 -13
View File
@@ -4,18 +4,18 @@ import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// Push pushes to a branch
type PushOpts struct {
Force bool
UpstreamRemote string
UpstreamBranch string
SetUpstream bool
PromptUserForCredential func(string) string
Force bool
UpstreamRemote string
UpstreamBranch string
SetUpstream bool
}
func (c *GitCommand) Push(opts PushOpts) error {
func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
cmdStr := "git push"
if opts.Force {
@@ -32,13 +32,22 @@ func (c *GitCommand) Push(opts PushOpts) error {
if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" {
return errors.New(c.Tr.MustSpecifyOriginError)
return nil, errors.New(c.Tr.MustSpecifyOriginError)
}
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
cmdObj := c.Cmd.New(cmdStr)
return cmdObj, nil
}
func (c *GitCommand) Push(opts PushOpts, promptUserForCredential func(string) string) error {
cmdObj, err := c.PushCmdObj(opts)
if err != nil {
return err
}
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
type FetchOptions struct {
@@ -58,7 +67,7 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, func(question string) string {
if opts.PromptUserForCredential != nil {
return opts.PromptUserForCredential(question)
@@ -94,18 +103,18 @@ func (c *GitCommand) Pull(opts PullOptions) error {
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
cmdObj := c.Cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
+42 -113
View File
@@ -1,164 +1,93 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
// TestGitCommandPush is a function.
func TestGitCommandPush(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
opts PushOpts
test func(error)
}
prompt := func(passOrUname string) string {
return "\n"
test func(oscommands.ICmdObj, error)
}
scenarios := []scenario{
{
"Push with force disabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return secureexec.Command("echo")
},
PushOpts{Force: false, PromptUserForCredential: prompt},
func(err error) {
testName: "Push with force disabled",
opts: PushOpts{Force: false},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push")
assert.NoError(t, err)
},
},
{
"Push with force enabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
return secureexec.Command("echo")
},
PushOpts{Force: true, PromptUserForCredential: prompt},
func(err error) {
testName: "Push with force enabled",
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
assert.NoError(t, err)
},
},
{
"Push with an error occurring",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return secureexec.Command("test")
testName: "Push with force disabled, upstream supplied",
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
},
PushOpts{Force: false, PromptUserForCredential: prompt},
func(err error) {
assert.Error(t, err)
},
},
{
"Push with force disabled, upstream supplied",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push "origin" "master"`)
assert.NoError(t, err)
},
},
{
"Push with force disabled, setting upstream",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--set-upstream", "origin", "master"}, args)
return secureexec.Command("echo")
testName: "Push with force disabled, setting upstream",
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
"Push with force enabled, setting upstream",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease", "--set-upstream", "origin", "master"}, args)
return secureexec.Command("echo")
testName: "Push with force enabled, setting upstream",
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
"Push with remote branch but no origin",
func(cmd string, args ...string) *exec.Cmd {
return nil
testName: "Push with remote branch but no origin",
opts: PushOpts{
Force: true,
UpstreamRemote: "",
UpstreamBranch: "master",
SetUpstream: true,
},
PushOpts{
Force: true,
UpstreamRemote: "",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Error(t, err)
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
},
},
{
"Push with force disabled, upstream supplied",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
err := gitCmd.Push(s.opts)
s.test(err)
gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t))
s.test(gitCmd.PushCmdObj(s.opts))
})
}
}
+7 -3
View File
@@ -5,15 +5,19 @@ import (
)
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
return c.RunCommand("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)
return c.Cmd.New(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)).Run()
}
func (c *GitCommand) CreateAnnotatedTag(tagName, commitSha, msg string) error {
return c.Cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))).Run()
}
func (c *GitCommand) DeleteTag(tagName string) error {
return c.RunCommand("git tag -d %s", c.OSCommand.Quote(tagName))
return c.Cmd.New(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName))).Run()
}
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
+14
View File
@@ -0,0 +1,14 @@
package enums
type RebaseMode int
const (
// this means we're neither rebasing nor merging
REBASE_MODE_NONE RebaseMode = iota
// this means normal rebase as opposed to interactive rebase
REBASE_MODE_NORMAL
REBASE_MODE_INTERACTIVE
// REBASE_MODE_REBASING is a general state that captures both REBASE_MODE_NORMAL and REBASE_MODE_INTERACTIVE
REBASE_MODE_REBASING
REBASE_MODE_MERGING
)
+15
View File
@@ -0,0 +1,15 @@
package common
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// Commonly used things wrapped into one struct for convenience when passing it around
type Common struct {
Log *logrus.Entry
Tr *i18n.TranslationSet
UserConfig *config.UserConfig
Debug bool
}
+5 -35
View File
@@ -31,21 +31,21 @@ type AppConfig struct {
// from AppConfig and still be used by lazygit.
type AppConfigurer interface {
GetDebug() bool
// build info
GetVersion() string
GetCommit() string
GetBuildDate() string
GetName() string
GetBuildSource() string
GetUserConfig() *UserConfig
GetUserConfigPaths() []string
GetUserConfigDir() string
GetTempDir() string
ReloadUserConfig() error
GetAppState() *AppState
SaveAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
ReloadUserConfig() error
ShowCommandLogOnStartup() bool
}
// NewAppConfig makes a new app config
@@ -167,37 +167,22 @@ func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error)
return base, nil
}
// GetIsNewRepo returns known repo boolean
func (c *AppConfig) GetIsNewRepo() bool {
return c.IsNewRepo
}
// SetIsNewRepo set if the current repo is known
func (c *AppConfig) SetIsNewRepo(toSet bool) {
c.IsNewRepo = toSet
}
// GetDebug returns debug flag
func (c *AppConfig) GetDebug() bool {
return c.Debug
}
// GetVersion returns debug flag
func (c *AppConfig) GetVersion() string {
return c.Version
}
// GetCommit returns debug flag
func (c *AppConfig) GetCommit() string {
return c.Commit
}
// GetBuildDate returns debug flag
func (c *AppConfig) GetBuildDate() string {
return c.BuildDate
}
// GetName returns debug flag
func (c *AppConfig) GetName() string {
return c.Name
}
@@ -226,10 +211,6 @@ func (c *AppConfig) GetUserConfigDir() string {
return c.UserConfigDir
}
func (c *AppConfig) GetTempDir() string {
return c.TempDir
}
func (c *AppConfig) ReloadUserConfig() error {
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
if err != nil {
@@ -277,17 +258,6 @@ func (c *AppConfig) SaveAppState() error {
return err
}
// originally we could only hide the command log permanently via the config
// but now we do it via state. So we need to still support the config for the
// sake of backwards compatibility
func (c *AppConfig) ShowCommandLogOnStartup() bool {
if !c.UserConfig.Gui.ShowCommandLog {
return false
}
return !c.AppState.HideCommandLog
}
// loadAppState loads recorded AppState from file
func loadAppState() (*AppState, error) {
filepath, err := configFilePath("state.yml")
+18 -2
View File
@@ -61,6 +61,7 @@ type CommitLengthConfig struct {
type GitConfig struct {
Paging PagingConfig `yaml:"paging"`
Commit CommitConfig `yaml:"commit"`
Merging MergingConfig `yaml:"merging"`
SkipHookPrefix string `yaml:"skipHookPrefix"`
AutoFetch bool `yaml:"autoFetch"`
@@ -70,8 +71,9 @@ type GitConfig struct {
DisableForcePushing bool `yaml:"disableForcePushing"`
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
// this shoudl really be under 'gui', not 'git'
ParseEmoji bool `yaml:"parseEmoji"`
Log LogConfig `yaml:"log"`
ParseEmoji bool `yaml:"parseEmoji"`
Log LogConfig `yaml:"log"`
DiffContextSize int `yaml:"diffContextSize"`
}
type PagingConfig struct {
@@ -80,6 +82,10 @@ type PagingConfig struct {
UseConfig bool `yaml:"useConfig"`
}
type CommitConfig struct {
SignOff bool `yaml:"signOff"`
}
type MergingConfig struct {
ManualCommit bool `yaml:"manualCommit"`
Args string `yaml:"args"`
@@ -176,6 +182,8 @@ type KeybindingUniversalConfig struct {
AppendNewline string `yaml:"appendNewline"`
ExtrasMenu string `yaml:"extrasMenu"`
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"`
DecreaseContextInDiffView string `yaml:"decreaseContextInDiffView"`
}
type KeybindingStatusConfig struct {
@@ -238,6 +246,7 @@ type KeybindingCommitsConfig struct {
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
OpenLogMenu string `yaml:"openLogMenu"`
OpenInBrowser string `yaml:"openInBrowser"`
}
type KeybindingStashConfig struct {
@@ -344,6 +353,9 @@ func GetDefaultConfig() *UserConfig {
ColorArg: "always",
Pager: "",
UseConfig: false},
Commit: CommitConfig{
SignOff: false,
},
Merging: MergingConfig{
ManualCommit: false,
Args: "",
@@ -359,6 +371,7 @@ func GetDefaultConfig() *UserConfig {
DisableForcePushing: false,
CommitPrefixes: map[string]CommitPrefixConfig(nil),
ParseEmoji: false,
DiffContextSize: 3,
},
Refresher: RefresherConfig{
RefreshInterval: 10,
@@ -436,6 +449,8 @@ func GetDefaultConfig() *UserConfig {
AppendNewline: "<a-enter>",
ExtrasMenu: "@",
ToggleWhitespaceInDiffView: "<c-w>",
IncreaseContextInDiffView: "}",
DecreaseContextInDiffView: "{",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
@@ -494,6 +509,7 @@ func GetDefaultConfig() *UserConfig {
ResetCherryPick: "<c-R>",
CopyCommitMessageToClipboard: "<c-y>",
OpenLogMenu: "<c-l>",
OpenInBrowser: "o",
},
Stash: KeybindingStashConfig{
PopStash: "g",
+4 -4
View File
@@ -44,7 +44,7 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.Config.GetUserConfig().Gui.SidePanelWidth
sidePanelWidthRatio := gui.UserConfig.Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
@@ -115,7 +115,7 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
return false
}
mainPanelSplitMode := gui.Config.GetUserConfig().Gui.MainPanelSplitMode
mainPanelSplitMode := gui.UserConfig.Gui.MainPanelSplitMode
width, height := gui.g.Size()
switch mainPanelSplitMode {
@@ -143,7 +143,7 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
} else if screenHeight < 40 {
baseSize = 1
} else {
baseSize = gui.Config.GetUserConfig().Gui.CommandLogSize
baseSize = gui.UserConfig.Gui.CommandLogSize
}
frameSize := 2
@@ -259,7 +259,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
fullHeightBox("stash"),
}
} else if height >= 28 {
accordionMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
accordionMode := gui.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
+52 -30
View File
@@ -1,9 +1,11 @@
package gui
type BasicContext struct {
OnFocus func() error
OnFocusLost func() error
OnRender func() error
OnFocus func(opts ...OnFocusOpts) error
OnFocusLost func() error
OnRender func() error
// this is for pushing some content to the main view
OnRenderToMain func(opts ...OnFocusOpts) error
Kind ContextKind
Key ContextKey
ViewName string
@@ -15,63 +17,83 @@ type BasicContext struct {
hasParent bool
}
func (c *BasicContext) GetOptionsMap() map[string]string {
if c.OnGetOptionsMap != nil {
return c.OnGetOptionsMap()
func (self *BasicContext) GetOptionsMap() map[string]string {
if self.OnGetOptionsMap != nil {
return self.OnGetOptionsMap()
}
return nil
}
func (c *BasicContext) SetParentContext(context Context) {
c.ParentContext = context
c.hasParent = true
func (self *BasicContext) SetParentContext(context Context) {
self.ParentContext = context
self.hasParent = true
}
func (c *BasicContext) GetParentContext() (Context, bool) {
return c.ParentContext, c.hasParent
func (self *BasicContext) GetParentContext() (Context, bool) {
return self.ParentContext, self.hasParent
}
func (c *BasicContext) SetWindowName(windowName string) {
c.WindowName = windowName
func (self *BasicContext) SetWindowName(windowName string) {
self.WindowName = windowName
}
func (c *BasicContext) GetWindowName() string {
windowName := c.WindowName
func (self *BasicContext) GetWindowName() string {
windowName := self.WindowName
if windowName != "" {
return windowName
}
// TODO: actually set this for everything so we don't default to the view name
return c.ViewName
return self.ViewName
}
func (c *BasicContext) HandleRender() error {
if c.OnRender != nil {
return c.OnRender()
func (self *BasicContext) HandleRender() error {
if self.OnRender != nil {
return self.OnRender()
}
return nil
}
func (c *BasicContext) GetViewName() string {
return c.ViewName
func (self *BasicContext) GetViewName() string {
return self.ViewName
}
func (c *BasicContext) HandleFocus() error {
return c.OnFocus()
func (self *BasicContext) HandleFocus(opts ...OnFocusOpts) error {
if self.OnFocus != nil {
if err := self.OnFocus(opts...); err != nil {
return err
}
}
if self.OnRenderToMain != nil {
if err := self.OnRenderToMain(opts...); err != nil {
return err
}
}
return nil
}
func (c *BasicContext) HandleFocusLost() error {
if c.OnFocusLost != nil {
return c.OnFocusLost()
func (self *BasicContext) HandleFocusLost() error {
if self.OnFocusLost != nil {
return self.OnFocusLost()
}
return nil
}
func (c *BasicContext) GetKind() ContextKind {
return c.Kind
func (self *BasicContext) HandleRenderToMain() error {
if self.OnRenderToMain != nil {
return self.OnRenderToMain()
}
return nil
}
func (c *BasicContext) GetKey() ContextKey {
return c.Key
func (self *BasicContext) GetKind() ContextKind {
return self.Kind
}
func (self *BasicContext) GetKey() ContextKey {
return self.Key
}
+19 -13
View File
@@ -1,6 +1,7 @@
package gui
import (
"errors"
"fmt"
"strings"
@@ -25,17 +26,15 @@ func (gui *Gui) getSelectedBranch() *models.Branch {
return gui.State.Branches[selectedLine]
}
func (gui *Gui) handleBranchSelect() error {
func (gui *Gui) branchesRenderToMain() error {
var task updateTask
branch := gui.getSelectedBranch()
if branch == nil {
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(branch.Name)
task = NewRunPtyTask(cmd)
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -56,17 +55,13 @@ func (gui *Gui) refreshBranches() {
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
reflogCommits, _, err = gui.GitCommand.Loaders.ReflogCommits.GetReflogCommits(nil, "")
if err != nil {
gui.Log.Error(err)
}
}
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
if err != nil {
_ = gui.surfaceError(err)
}
gui.State.Branches = builder.Build()
gui.State.Branches = gui.GitCommand.Loaders.Branches.Load(reflogCommits)
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
gui.Log.Error(err)
@@ -104,13 +99,24 @@ func (gui *Gui) handleCreatePullRequestMenu() error {
}
func (gui *Gui) handleCopyPullRequestURLPress() error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
hostingServiceMgr := gui.getHostingServiceMgr()
branch := gui.getSelectedBranch()
url, err := pullRequest.CopyURL(branch.Name, "")
branchExistsOnRemote := gui.GitCommand.CheckRemoteBranchExists(branch.Name)
if !branchExistsOnRemote {
return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote))
}
url, err := hostingServiceMgr.GetPullRequestURL(branch.Name, "")
if err != nil {
return gui.surfaceError(err)
}
if err := gui.GitCommand.OSCommand.CopyToClipboard(url); err != nil {
return gui.surfaceError(err)
}
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Copying to clipboard: '%s'", url), "Copy URL", false))
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
+3 -3
View File
@@ -41,11 +41,11 @@ func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
func (gui *Gui) printCommandLogHeader() {
introStr := fmt.Sprintf(
gui.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.ExtrasMenu),
)
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
if gui.UserConfig.Gui.ShowRandomTip {
fmt.Fprintf(
gui.Views.Extras,
"%s: %s",
@@ -56,7 +56,7 @@ func (gui *Gui) printCommandLogHeader() {
}
func (gui *Gui) getRandomTip() string {
config := gui.Config.GetUserConfig().Keybinding
config := gui.UserConfig.Keybinding
formattedKey := func(key string) string {
return gui.getKeyDisplay(key)
+13 -15
View File
@@ -31,9 +31,12 @@ func (gui *Gui) getSelectedCommitFilePath() string {
return node.GetPath()
}
func (gui *Gui) handleCommitFileSelect() error {
func (gui *Gui) onCommitFileFocus() error {
gui.escapeLineByLinePanel()
return nil
}
func (gui *Gui) commitFilesRenderToMain() error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
@@ -42,10 +45,8 @@ func (gui *Gui) handleCommitFileSelect() error {
to := gui.State.CommitFileManager.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, node.GetPath(), false),
)
task := NewRunPtyTask(cmd)
cmdObj := gui.GitCommand.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -104,7 +105,7 @@ func (gui *Gui) refreshCommitFilesView() error {
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse)
files, err := gui.GitCommand.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse)
if err != nil {
return gui.surfaceError(err)
}
@@ -198,10 +199,10 @@ func (gui *Gui) startPatchManager() error {
}
func (gui *Gui) handleEnterCommitFile() error {
return gui.enterCommitFile(-1)
return gui.enterCommitFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
node := gui.getSelectedCommitFileNode()
if node == nil {
return nil
@@ -211,17 +212,14 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return gui.handleToggleCommitFileDirCollapsed()
}
enterTheFile := func(selectedLineIdx int) error {
enterTheFile := func() error {
if !gui.GitCommand.PatchManager.Active() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
if err := gui.pushContext(gui.State.Contexts.PatchBuilding); err != nil {
return err
}
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
return gui.pushContext(gui.State.Contexts.PatchBuilding, opts)
}
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
@@ -231,7 +229,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
handlersManageFocus: true,
handleConfirm: func() error {
gui.GitCommand.PatchManager.Reset()
return enterTheFile(selectedLineIdx)
return enterTheFile()
},
handleClose: func() error {
return gui.pushContext(gui.State.Contexts.CommitFiles)
@@ -239,7 +237,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
})
}
return enterTheFile(selectedLineIdx)
return enterTheFile()
}
func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
+15 -10
View File
@@ -14,16 +14,21 @@ func (gui *Gui) handleCommitConfirm() error {
if message == "" {
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
}
flags := ""
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
flags := []string{}
skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
flags = "--no-verify"
flags = append(flags, "--no-verify")
}
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
if gui.UserConfig.Git.Commit.SignOff {
flags = append(flags, "--signoff")
}
cmdObj := gui.GitCommand.CommitCmdObj(message, strings.Join(flags, " "))
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdObj.ToString(), gui.Tr.Spans.Commit, true))
_ = gui.returnFromContext()
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
return gui.withGpgHandling(cmdObj, gui.Tr.CommittingStatus, func() error {
gui.Views.CommitMessage.ClearTextArea()
return nil
})
@@ -37,9 +42,9 @@ func (gui *Gui) handleCommitMessageFocused() error {
message := utils.ResolvePlaceholderString(
gui.Tr.CommitMessageConfirm,
map[string]string{
"keyBindClose": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline),
"keyBindClose": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.AppendNewline),
},
)
@@ -53,7 +58,7 @@ func (gui *Gui) getBufferLength(view *gocui.View) string {
// RenderCommitLength is a function.
func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
if !gui.UserConfig.Gui.CommitLength.Show {
return
}
+80 -22
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -24,7 +24,7 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
return gui.State.Commits[selectedLine]
}
func (gui *Gui) handleCommitSelect() error {
func (gui *Gui) onCommitFocus() error {
state := gui.State.Panels.Commits
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
state.LimitCommits = false
@@ -37,15 +37,17 @@ func (gui *Gui) handleCommitSelect() error {
gui.escapeLineByLinePanel()
return nil
}
func (gui *Gui) branchCommitsRenderToMain() error {
var task updateTask
commit := gui.getSelectedLocalCommit()
if commit == nil {
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
)
task = NewRunPtyTask(cmd)
cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -117,10 +119,8 @@ func (gui *Gui) refreshCommitsWithLimit() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
commits, err := builder.GetCommits(
commands.GetCommitsOptions{
commits, err := gui.GitCommand.Loaders.Commits.GetCommits(
loaders.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.GetPath(),
IncludeRebaseCommits: true,
@@ -140,9 +140,7 @@ func (gui *Gui) refreshRebaseCommits() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
updatedCommits, err := builder.MergeRebasingCommits(gui.State.Commits)
updatedCommits, err := gui.GitCommand.Loaders.Commits.MergeRebasingCommits(gui.State.Commits)
if err != nil {
return err
}
@@ -575,25 +573,63 @@ func (gui *Gui) handleSquashAllAboveFixupCommits() error {
}
func (gui *Gui) handleTagCommit() error {
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
// if annotated, switch to a subprocess to create the message
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
return gui.handleCreateLightweightTag(commit.Sha)
return gui.createTagMenu(commit.Sha)
}
func (gui *Gui) createTagMenu(commitSha string) error {
items := []*menuItem{
{
displayString: gui.Tr.LcLightweightTag,
onPress: func() error {
return gui.handleCreateLightweightTag(commitSha)
},
},
{
displayString: gui.Tr.LcAnnotatedTag,
onPress: func() error {
return gui.handleCreateAnnotatedTag(commitSha)
},
},
}
return gui.createMenu(gui.Tr.TagMenuTitle, items, createMenuOptions{showCancel: true})
}
func (gui *Gui) afterTagCreate(tagName string) error {
gui.State.Panels.Tags.SelectedLineIdx = 0 // Set to the top
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
}
func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error {
return gui.prompt(promptOpts{
title: gui.Tr.TagNameTitle,
handleConfirm: func(tagName string) error {
return gui.prompt(promptOpts{
title: gui.Tr.TagMessageTitle,
handleConfirm: func(msg string) error {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateAnnotatedTag).CreateAnnotatedTag(tagName, commitSha, msg); err != nil {
return gui.surfaceError(err)
}
return gui.afterTagCreate(tagName)
},
})
},
})
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.prompt(promptOpts{
title: gui.Tr.TagNameTitle,
handleConfirm: func(response string) error {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(response, commitSha); err != nil {
handleConfirm: func(tagName string) error {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(tagName, commitSha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
return gui.afterTagCreate(tagName)
},
})
}
@@ -693,7 +729,7 @@ func (gui *Gui) handleOpenLogMenu() error {
opensMenu: true,
onPress: func() error {
onSelect := func(value string) {
gui.Config.GetUserConfig().Git.Log.ShowGraph = value
gui.UserConfig.Git.Log.ShowGraph = value
gui.render()
}
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
@@ -726,7 +762,7 @@ func (gui *Gui) handleOpenLogMenu() error {
opensMenu: true,
onPress: func() error {
onSelect := func(value string) error {
gui.Config.GetUserConfig().Git.Log.Order = value
gui.UserConfig.Git.Log.Order = value
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
})
@@ -755,3 +791,25 @@ func (gui *Gui) handleOpenLogMenu() error {
},
}, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleOpenCommitInBrowser() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
hostingServiceMgr := gui.getHostingServiceMgr()
url, err := hostingServiceMgr.GetCommitURL(commit.Sha)
if err != nil {
return gui.surfaceError(err)
}
if err := gui.GitCommand.OSCommand.OpenLink(url); err != nil {
return gui.surfaceError(err)
}
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.OpeningCommitInBrowser, url), gui.Tr.CreatePullRequest, false))
return nil
}
+7 -33
View File
@@ -1,5 +1,3 @@
// lots of this has been directly ported from one of the example files, will brush up later
package gui
import (
@@ -7,7 +5,6 @@ import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -44,30 +41,15 @@ type promptOpts struct {
}
func (gui *Gui) ask(opts askOpts) error {
return gui.createPopupPanel(createPopupPanelOpts{
title: opts.title,
prompt: opts.prompt,
handleConfirm: opts.handleConfirm,
handleClose: opts.handleClose,
handlersManageFocus: opts.handlersManageFocus,
})
return gui.PopupHandler.Ask(opts)
}
func (gui *Gui) prompt(opts promptOpts) error {
return gui.createPopupPanel(createPopupPanelOpts{
title: opts.title,
prompt: opts.initialContent,
editable: true,
handleConfirmPrompt: opts.handleConfirm,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
return gui.PopupHandler.Prompt(opts)
}
func (gui *Gui) createLoaderPanel(prompt string) error {
return gui.createPopupPanel(createPopupPanelOpts{
prompt: prompt,
hasLoader: true,
})
return gui.PopupHandler.Loader(prompt)
}
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error {
@@ -191,7 +173,7 @@ func (gui *Gui) prepareConfirmationPanel(
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions(findSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.UserConfig.Keybinding.Universal.TogglePanel)
}
gui.g.Update(func(g *gocui.Gui) error {
@@ -258,7 +240,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
handler func() error
}
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
opts.handlersManageFocus,
opts.handleConfirmPrompt,
@@ -323,7 +305,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
@@ -339,15 +321,7 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
}
func (gui *Gui) createErrorPanel(message string) error {
coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message))
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
return gui.ask(askOpts{
title: gui.Tr.Error,
prompt: coloredMessage,
})
return gui.PopupHandler.Error(message)
}
func (gui *Gui) surfaceError(err error) error {
+20 -8
View File
@@ -1,6 +1,7 @@
package gui
import (
"errors"
"fmt"
"github.com/jesseduffield/gocui"
@@ -16,10 +17,16 @@ const (
EXTRAS_CONTEXT
)
type OnFocusOpts struct {
ClickedViewName string
ClickedViewLineIdx int
}
type Context interface {
HandleFocus() error
HandleFocus(opts ...OnFocusOpts) error
HandleFocusLost() error
HandleRender() error
HandleRenderToMain() error
GetKind() ContextKind
GetViewName() string
GetWindowName() string
@@ -81,15 +88,20 @@ func (gui *Gui) replaceContext(c Context) error {
return nil
}
func (gui *Gui) pushContext(c Context) error {
func (gui *Gui) pushContext(c Context, opts ...OnFocusOpts) error {
// using triple dot but you should only ever pass one of these opt structs
if len(opts) > 1 {
return errors.New("cannot pass multiple opts to pushContext")
}
gui.g.Update(func(*gocui.Gui) error {
return gui.pushContextDirect(c)
return gui.pushContextDirect(c, opts...)
})
return nil
}
func (gui *Gui) pushContextDirect(c Context) error {
func (gui *Gui) pushContextDirect(c Context, opts ...OnFocusOpts) error {
gui.State.ContextManager.Lock()
// push onto stack
@@ -114,7 +126,7 @@ func (gui *Gui) pushContextDirect(c Context) error {
gui.State.ContextManager.Unlock()
return gui.activateContext(c)
return gui.activateContext(c, opts...)
}
// asynchronous code idea: functions return an error via a channel, when done
@@ -169,7 +181,7 @@ func (gui *Gui) deactivateContext(c Context) error {
}
// if we are the kind of context that is sent to back upon deactivation, we should do that
if view != nil && c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY {
if view != nil && (c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY) {
view.Visible = false
}
@@ -206,7 +218,7 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
return nil
}
func (gui *Gui) activateContext(c Context) error {
func (gui *Gui) activateContext(c Context, opts ...OnFocusOpts) error {
viewName := c.GetViewName()
v, err := gui.g.View(viewName)
if err != nil {
@@ -249,7 +261,7 @@ func (gui *Gui) activateContext(c Context) error {
}
gui.renderOptionsMap(optionsMap)
if err := c.HandleFocus(); err != nil {
if err := c.HandleFocus(opts...); err != nil {
return err
}
+35 -19
View File
@@ -110,10 +110,10 @@ func (gui *Gui) allContexts() []Context {
func (gui *Gui) contextTree() ContextTree {
return ContextTree{
Status: &BasicContext{
OnFocus: gui.handleStatusSelect,
Kind: SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
OnRenderToMain: OnFocusWrapper(gui.statusRenderToMain),
Kind: SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
},
Files: gui.filesListContext(),
Submodules: gui.submodulesListContext(),
@@ -128,7 +128,7 @@ func (gui *Gui) contextTree() ContextTree {
Tags: gui.tagsListContext(),
Stash: gui.stashListContext(),
Normal: &BasicContext{
OnFocus: func() error {
OnFocus: func(opts ...OnFocusOpts) error {
return nil // TODO: should we do something here? We should allow for scrolling the panel
},
Kind: MAIN_CONTEXT,
@@ -136,59 +136,67 @@ func (gui *Gui) contextTree() ContextTree {
Key: MAIN_NORMAL_CONTEXT_KEY,
},
Staging: &BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshStagingPanel(false, -1)
OnRenderToMain: func(opts ...OnFocusOpts) error {
forceSecondaryFocused := false
selectedLineIdx := -1
if len(opts) > 0 && opts[0].ClickedViewName != "" {
if opts[0].ClickedViewName == "main" || opts[0].ClickedViewName == "secondary" {
selectedLineIdx = opts[0].ClickedViewLineIdx
}
if opts[0].ClickedViewName == "secondary" {
forceSecondaryFocused = true
}
}
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_STAGING_CONTEXT_KEY,
},
PatchBuilding: &BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshPatchBuildingPanel(-1)
OnRenderToMain: func(opts ...OnFocusOpts) error {
selectedLineIdx := -1
if len(opts) > 0 && (opts[0].ClickedViewName == "main" || opts[0].ClickedViewName == "secondary") {
selectedLineIdx = opts[0].ClickedViewLineIdx
}
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
},
Merging: &BasicContext{
OnFocus: gui.refreshMergePanelWithLock,
OnFocus: OnFocusWrapper(gui.refreshMergePanelWithLock),
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
},
Credentials: &BasicContext{
OnFocus: gui.handleCredentialsViewFocused,
OnFocus: OnFocusWrapper(gui.handleCredentialsViewFocused),
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
},
Confirmation: &BasicContext{
OnFocus: func() error { return nil },
Kind: TEMPORARY_POPUP,
ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
},
Suggestions: gui.suggestionsListContext(),
CommitMessage: &BasicContext{
OnFocus: gui.handleCommitMessageFocused,
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
},
Search: &BasicContext{
OnFocus: func() error { return nil },
Kind: PERSISTENT_POPUP,
ViewName: "search",
Key: SEARCH_CONTEXT_KEY,
},
CommandLog: &BasicContext{
OnFocus: func() error { return nil },
Kind: EXTRAS_CONTEXT,
ViewName: "extras",
Key: COMMAND_LOG_CONTEXT_KEY,
@@ -201,6 +209,14 @@ func (gui *Gui) contextTree() ContextTree {
}
}
// using this wrapper for when an onFocus function doesn't care about any potential
// props that could be passed
func OnFocusWrapper(f func() error) func(opts ...OnFocusOpts) error {
return func(opts ...OnFocusOpts) error {
return f()
}
}
func (tree ContextTree) initialViewContextMap() map[string]Context {
return map[string]Context{
"status": tree.Status,
+40
View File
@@ -0,0 +1,40 @@
package gui
import (
"testing"
"github.com/jesseduffield/gocui"
"github.com/stretchr/testify/assert"
)
func TestCanDeactivatePopupContextsWithoutViews(t *testing.T) {
contexts := []func(gui *Gui) Context{
func(gui *Gui) Context { return gui.State.Contexts.Credentials },
func(gui *Gui) Context { return gui.State.Contexts.Confirmation },
func(gui *Gui) Context { return gui.State.Contexts.CommitMessage },
func(gui *Gui) Context { return gui.State.Contexts.Search },
}
for _, c := range contexts {
gui := NewDummyGui()
context := c(gui)
gui.g = &gocui.Gui{}
_ = gui.deactivateContext(context)
// This really only checks a prerequisit, not the effect of deactivateContext
view, _ := gui.g.View(context.GetViewName())
assert.Nil(t, view, string(context.GetKey()))
}
}
func TestCanDeactivateCommitFilesContextsWithoutViews(t *testing.T) {
gui := NewDummyGui()
gui.g = &gocui.Gui{}
_ = gui.deactivateContext(gui.State.Contexts.CommitFiles)
// This really only checks a prerequisite, not the effect of deactivateContext
view, _ := gui.g.View(gui.State.Contexts.CommitFiles.GetViewName())
assert.Nil(t, view)
}
+1 -1
View File
@@ -57,7 +57,7 @@ func (gui *Gui) handleCloseCredentialsView() error {
}
func (gui *Gui) handleCredentialsViewFocused() error {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
message := utils.ResolvePlaceholderString(
gui.Tr.CloseConfirm,
+5 -4
View File
@@ -203,7 +203,7 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
}
// Run and save output
message, err := gui.GitCommand.RunCommandWithOutput(cmdStr)
message, err := gui.GitCommand.Cmd.New(cmdStr).RunWithOutput()
if err != nil {
return gui.surfaceError(err)
}
@@ -244,7 +244,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
}
if customCommand.Subprocess {
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr))
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.Cmd.NewShell(cmdStr))
}
loadingText := customCommand.LoadingText
@@ -252,7 +252,8 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
loadingText = gui.Tr.LcRunningCustomCommandStatus
}
return gui.WithWaitingStatus(loadingText, func() error {
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).RunShellCommand(cmdStr); err != nil {
err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).Cmd.NewShell(cmdStr).Run()
if err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{})
@@ -295,7 +296,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
bindings := []*Binding{}
customCommands := gui.Config.GetUserConfig().CustomCommands
customCommands := gui.UserConfig.CustomCommands
for _, customCommand := range customCommands {
var viewName string
+47
View File
@@ -0,0 +1,47 @@
package gui
import (
"errors"
)
func isShowingDiff(gui *Gui) bool {
key := gui.currentStaticContext().GetKey()
return key == FILES_CONTEXT_KEY || key == COMMIT_FILES_CONTEXT_KEY || key == STASH_CONTEXT_KEY || key == BRANCH_COMMITS_CONTEXT_KEY || key == SUB_COMMITS_CONTEXT_KEY || key == MAIN_STAGING_CONTEXT_KEY || key == MAIN_PATCH_BUILDING_CONTEXT_KEY
}
func (gui *Gui) IncreaseContextInDiffView() error {
if isShowingDiff(gui) {
if err := gui.CheckCanChangeContext(); err != nil {
return gui.surfaceError(err)
}
gui.UserConfig.Git.DiffContextSize = gui.UserConfig.Git.DiffContextSize + 1
return gui.currentStaticContext().HandleRenderToMain()
}
return nil
}
func (gui *Gui) DecreaseContextInDiffView() error {
old_size := gui.UserConfig.Git.DiffContextSize
if isShowingDiff(gui) && old_size > 1 {
if err := gui.CheckCanChangeContext(); err != nil {
return gui.surfaceError(err)
}
gui.UserConfig.Git.DiffContextSize = old_size - 1
return gui.currentStaticContext().HandleRenderToMain()
}
return nil
}
func (gui *Gui) CheckCanChangeContext() error {
if gui.GitCommand.PatchManager.Active() {
return errors.New(gui.Tr.CantChangeContextSizeError)
}
return nil
}
+185
View File
@@ -0,0 +1,185 @@
package gui
import (
"testing"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/stretchr/testify/assert"
)
const diffForTest = `diff --git a/pkg/gui/diff_context_size.go b/pkg/gui/diff_context_size.go
index 0da0a982..742b7dcf 100644
--- a/pkg/gui/diff_context_size.go
+++ b/pkg/gui/diff_context_size.go
@@ -9,12 +9,12 @@ func getRefreshFunction(gui *Gui) func()error {
}
} else if key == MAIN_STAGING_CONTEXT_KEY {
return func() error {
- selectedLine := gui.Views.Secondary.SelectedLineIdx()
+ selectedLine := gui.State.Panels.LineByLine.GetSelectedLineIdx()
return gui.handleRefreshStagingPanel(false, selectedLine)
}
} else if key == MAIN_PATCH_BUILDING_CONTEXT_KEY {
`
func setupGuiForTest(gui *Gui) {
gui.g = &gocui.Gui{}
gui.Views.Main, _ = gui.prepareView("main")
gui.Views.Secondary, _ = gui.prepareView("secondary")
gui.GitCommand.PatchManager = &patch.PatchManager{}
_, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11)
}
func TestIncreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
contexts := []func(gui *Gui) Context{
func(gui *Gui) Context { return gui.State.Contexts.Files },
func(gui *Gui) Context { return gui.State.Contexts.BranchCommits },
func(gui *Gui) Context { return gui.State.Contexts.CommitFiles },
func(gui *Gui) Context { return gui.State.Contexts.Stash },
func(gui *Gui) Context { return gui.State.Contexts.Staging },
func(gui *Gui) Context { return gui.State.Contexts.PatchBuilding },
func(gui *Gui) Context { return gui.State.Contexts.SubCommits },
}
for _, c := range contexts {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContextDirect(context)
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
func TestDoesntIncreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
contexts := []func(gui *Gui) Context{
func(gui *Gui) Context { return gui.State.Contexts.Status },
func(gui *Gui) Context { return gui.State.Contexts.Submodules },
func(gui *Gui) Context { return gui.State.Contexts.Remotes },
func(gui *Gui) Context { return gui.State.Contexts.Normal },
func(gui *Gui) Context { return gui.State.Contexts.ReflogCommits },
func(gui *Gui) Context { return gui.State.Contexts.RemoteBranches },
func(gui *Gui) Context { return gui.State.Contexts.Tags },
func(gui *Gui) Context { return gui.State.Contexts.Merging },
func(gui *Gui) Context { return gui.State.Contexts.CommandLog },
}
for _, c := range contexts {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContextDirect(context)
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
func TestDecreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
contexts := []func(gui *Gui) Context{
func(gui *Gui) Context { return gui.State.Contexts.Files },
func(gui *Gui) Context { return gui.State.Contexts.BranchCommits },
func(gui *Gui) Context { return gui.State.Contexts.CommitFiles },
func(gui *Gui) Context { return gui.State.Contexts.Stash },
func(gui *Gui) Context { return gui.State.Contexts.Staging },
func(gui *Gui) Context { return gui.State.Contexts.PatchBuilding },
func(gui *Gui) Context { return gui.State.Contexts.SubCommits },
}
for _, c := range contexts {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(context)
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
func TestDoesntDecreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
contexts := []func(gui *Gui) Context{
func(gui *Gui) Context { return gui.State.Contexts.Status },
func(gui *Gui) Context { return gui.State.Contexts.Submodules },
func(gui *Gui) Context { return gui.State.Contexts.Remotes },
func(gui *Gui) Context { return gui.State.Contexts.Normal },
func(gui *Gui) Context { return gui.State.Contexts.ReflogCommits },
func(gui *Gui) Context { return gui.State.Contexts.RemoteBranches },
func(gui *Gui) Context { return gui.State.Contexts.Tags },
func(gui *Gui) Context { return gui.State.Contexts.Merging },
func(gui *Gui) Context { return gui.State.Contexts.CommandLog },
}
for _, c := range contexts {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(context)
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles)
gui.GitCommand.PatchManager.Start("from", "to", false, false)
errorCount := 0
gui.PopupHandler = &TestPopupHandler{
onError: func(message string) error {
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message)
errorCount += 1
return nil
},
}
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 1, errorCount)
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
}
func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles)
gui.GitCommand.PatchManager.Start("from", "to", false, false)
errorCount := 0
gui.PopupHandler = &TestPopupHandler{
onError: func(message string) error {
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message)
errorCount += 1
return nil
},
}
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
}
func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize)
}
+2 -2
View File
@@ -13,10 +13,10 @@ func (gui *Gui) exitDiffMode() error {
}
func (gui *Gui) renderDiff() error {
cmd := gui.OSCommand.ExecutableFromString(
cmdObj := gui.OSCommand.Cmd.New(
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()),
)
task := NewRunPtyTask(cmd)
task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
+4 -5
View File
@@ -4,7 +4,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -12,12 +11,12 @@ import (
// NewDummyGui creates a new dummy GUI for testing
func NewDummyUpdater() *updates.Updater {
newAppConfig := config.NewDummyAppConfig()
DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), newAppConfig, oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language))
return DummyUpdater
dummyUpdater, _ := updates.NewUpdater(utils.NewDummyCommon(), newAppConfig, oscommands.NewDummyOSCommand())
return dummyUpdater
}
func NewDummyGui() *Gui {
newAppConfig := config.NewDummyAppConfig()
DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, NewDummyUpdater(), "", false)
return DummyGui
dummyGui, _ := NewGui(utils.NewDummyCommon(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), newAppConfig, NewDummyUpdater(), "", false)
return dummyGui
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
newlineKey, ok := gui.getKey(gui.UserConfig.Keybinding.Universal.AppendNewline).(gocui.Key)
if !ok {
newlineKey = gocui.KeyAltEnter
}
+67 -44
View File
@@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -42,9 +43,7 @@ func (gui *Gui) getSelectedPath() string {
return node.GetPath()
}
func (gui *Gui) selectFile(alreadySelected bool) error {
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
func (gui *Gui) filesRenderToMain() error {
node := gui.getSelectedFileNode()
if node == nil {
@@ -56,30 +55,24 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
})
}
if !alreadySelected {
gui.takeOverMergeConflictScrolling()
}
if node.File != nil && node.File.HasInlineMergeConflicts {
return gui.refreshMergePanelWithLock()
}
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges,
task: NewRunPtyTask(cmd),
task: NewRunPtyTask(cmdObj.GetCmd()),
}}
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
task: NewRunPtyTask(cmd),
task: NewRunPtyTask(cmdObj.GetCmd()),
}
}
} else {
@@ -121,9 +114,12 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.Views.Main && ContextKey(g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
newSelectedPath := gui.getSelectedPath()
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
if err := gui.selectFile(alreadySelected); err != nil {
return err
if !alreadySelected {
gui.takeOverMergeConflictScrolling()
}
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
return gui.filesRenderToMain()
}
return nil
@@ -166,10 +162,10 @@ func (gui *Gui) stageSelectedFile() error {
}
func (gui *Gui) handleEnterFile() error {
return gui.enterFile(false, -1)
return gui.enterFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
func (gui *Gui) enterFile(opts OnFocusOpts) error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
@@ -193,9 +189,8 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
}
_ = gui.pushContext(gui.State.Contexts.Staging)
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
return gui.pushContext(gui.State.Contexts.Staging, opts)
}
func (gui *Gui) handleFilePress() error {
@@ -243,7 +238,7 @@ func (gui *Gui) handleFilePress() error {
return err
}
return gui.selectFile(true)
return gui.State.Contexts.Files.HandleFocus()
}
func (gui *Gui) allFilesStaged() bool {
@@ -255,8 +250,9 @@ func (gui *Gui) allFilesStaged() bool {
return true
}
func (gui *Gui) focusAndSelectFile() error {
return gui.selectFile(false)
func (gui *Gui) onFocusFile() error {
gui.takeOverMergeConflictScrolling()
return nil
}
func (gui *Gui) handleStageAll() error {
@@ -274,7 +270,7 @@ func (gui *Gui) handleStageAll() error {
return err
}
return gui.selectFile(false)
return gui.State.Contexts.Files.HandleFocus()
}
func (gui *Gui) handleIgnoreFile() error {
@@ -335,7 +331,7 @@ func (gui *Gui) handleIgnoreFile() error {
}
func (gui *Gui) handleWIPCommitPress() error {
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix
if skipHookPrefix == "" {
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
}
@@ -349,7 +345,7 @@ func (gui *Gui) handleWIPCommitPress() error {
}
func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
cfg, ok := gui.Config.GetUserConfig().Git.CommitPrefixes[utils.GetCurrentRepoName()]
cfg, ok := gui.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()]
if !ok {
return nil
}
@@ -359,7 +355,7 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
func (gui *Gui) prepareFilesForCommit() error {
noStagedFiles := len(gui.stagedFiles()) == 0
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
if err != nil {
return err
@@ -443,9 +439,9 @@ func (gui *Gui) handleAmendCommitPress() error {
title: strings.Title(gui.Tr.AmendLastCommit),
prompt: gui.Tr.SureToAmend,
handleConfirm: func() error {
cmdStr := gui.GitCommand.AmendHeadCmdStr()
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.AmendCommit, true))
return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil)
cmdObj := gui.GitCommand.AmendHeadCmdObj()
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdObj.ToString(), gui.Tr.Spans.AmendCommit, true))
return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil)
},
})
}
@@ -461,8 +457,16 @@ func (gui *Gui) handleCommitEditorPress() error {
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
}
args := []string{"commit"}
if gui.UserConfig.Git.Commit.SignOff {
args = append(args, "--signoff")
}
cmdStr := "git " + strings.Join(args, " ")
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.Commit).PrepareSubProcess("git", "commit"),
gui.GitCommand.WithSpan(gui.Tr.Spans.Commit).Cmd.New(cmdStr).Log(),
)
}
@@ -508,7 +512,7 @@ func (gui *Gui) editFileAtLine(filename string, lineNumber int) error {
}
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).ShellCommandFromString(cmdStr),
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).Cmd.NewShell(cmdStr),
)
}
@@ -550,7 +554,9 @@ func (gui *Gui) refreshStateFiles() error {
prevNodes := gui.State.FileManager.GetAllItems()
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
files := loaders.
NewFileLoader(gui.Common, gui.GitCommand.Cmd, gui.GitCommand.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{})
// for when you stage the old file of a rename and the new file is in a collapsed dir
state.FileManager.RWMutex.Lock()
@@ -657,9 +663,11 @@ func (gui *Gui) handlePullFiles() error {
}
}
suggestedRemote := getSuggestedRemote(gui.State.Remotes)
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin/" + currentBranch.Name,
initialContent: suggestedRemote + "/" + currentBranch.Name,
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"),
handleConfirm: func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
@@ -728,15 +736,14 @@ func (gui *Gui) push(opts pushOpts) error {
}
go utils.Safe(func() {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(commands.PushOpts{
Force: opts.force,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
PromptUserForCredential: gui.promptUserForCredential,
})
Force: opts.force,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
}, gui.promptUserForCredential)
if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
return
@@ -794,12 +801,14 @@ func (gui *Gui) pushFiles() error {
)
}
suggestedRemote := getSuggestedRemote(gui.State.Remotes)
if gui.GitCommand.PushToCurrent {
return gui.push(pushOpts{setUpstream: true})
} else {
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
initialContent: suggestedRemote + " " + currentBranch.Name,
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
handleConfirm: func(upstream string) error {
var upstreamBranch, upstreamRemote string
@@ -824,8 +833,22 @@ func (gui *Gui) pushFiles() error {
}
}
func getSuggestedRemote(remotes []*models.Remote) string {
if len(remotes) == 0 {
return "origin"
}
for _, remote := range remotes {
if remote.Name == "origin" {
return remote.Name
}
}
return remotes[0].Name
}
func (gui *Gui) requestToForcePush() error {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
return gui.createErrorPanel(gui.Tr.ForcePushDisabled)
}
@@ -902,7 +925,7 @@ func (gui *Gui) handleCustomCommand() error {
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.PrepareShellSubProcess(command),
gui.OSCommand.Cmd.NewShell(command),
)
},
})
@@ -983,7 +1006,7 @@ func (gui *Gui) handleOpenMergeTool() error {
prompt: gui.Tr.MergeToolPrompt,
handleConfirm: func() error {
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.ExecutableFromString(gui.GitCommand.OpenMergeToolCmd()),
gui.GitCommand.OpenMergeToolCmdObj(),
)
},
})
+34
View File
@@ -0,0 +1,34 @@
package gui
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
func TestGetSuggestedRemote(t *testing.T) {
cases := []struct {
remotes []*models.Remote
expected string
}{
{mkRemoteList(), "origin"},
{mkRemoteList("upstream", "origin", "foo"), "origin"},
{mkRemoteList("upstream", "foo", "bar"), "upstream"},
}
for _, c := range cases {
result := getSuggestedRemote(c.remotes)
assert.EqualValues(t, c.expected, result)
}
}
func mkRemoteList(names ...string) []*models.Remote {
var result []*models.Remote
for _, name := range names {
result = append(result, &models.Remote{Name: name})
}
return result
}
+3 -3
View File
@@ -32,7 +32,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
}
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).PrepareSubProcess("git", "flow", branchType, "finish", suffix),
gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).Cmd.New("git flow " + branchType + " finish " + suffix).Log(),
)
}
@@ -43,7 +43,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
}
// get config
gitFlowConfig, err := gui.GitCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
gitFlowConfig, err := gui.GitCommand.Cmd.New("git config --local --get-regexp gitflow").RunWithOutput()
if err != nil {
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
}
@@ -56,7 +56,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
title: title,
handleConfirm: func(name string) error {
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowStart).PrepareSubProcess("git", "flow", branchType, "start", name),
gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowStart).Cmd.New("git flow " + branchType + " start " + name).Log(),
)
},
})
+6 -6
View File
@@ -64,7 +64,7 @@ func (gui *Gui) prevScreenMode() error {
func (gui *Gui) scrollUpView(view *gocui.View) error {
ox, oy := view.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().Gui.ScrollHeight)))
newOy := int(math.Max(0, float64(oy-gui.UserConfig.Gui.ScrollHeight)))
return view.SetOrigin(ox, newOy)
}
@@ -86,12 +86,12 @@ func (gui *Gui) scrollDownView(view *gocui.View) error {
func (gui *Gui) linesToScrollDown(view *gocui.View) int {
_, oy := view.Origin()
y := oy
canScrollPastBottom := gui.Config.GetUserConfig().Gui.ScrollPastBottom
canScrollPastBottom := gui.UserConfig.Gui.ScrollPastBottom
if !canScrollPastBottom {
_, sy := view.Size()
y += sy
}
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
scrollHeight := gui.UserConfig.Gui.ScrollHeight
scrollableLines := view.ViewLinesHeight() - y
if scrollableLines < 0 {
return 0
@@ -189,9 +189,9 @@ func (gui *Gui) handleMouseDownMain() error {
// set filename, set primary/secondary selected, set line number, then switch context
// I'll need to know it was changed though.
// Could I pass something along to the context change?
return gui.enterFile(false, gui.Views.Main.SelectedLineIdx())
return gui.enterFile(OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
case gui.State.Contexts.CommitFiles:
return gui.enterCommitFile(gui.Views.Main.SelectedLineIdx())
return gui.enterCommitFile(OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
}
return nil
@@ -204,7 +204,7 @@ func (gui *Gui) handleMouseDownSecondary() error {
switch gui.g.CurrentView() {
case gui.Views.Files:
return gui.enterFile(true, gui.Views.Secondary.SelectedLineIdx())
return gui.enterFile(OnFocusOpts{ClickedViewName: "secondary", ClickedViewLineIdx: gui.Views.Secondary.SelectedLineIdx()})
}
return nil
+10 -9
View File
@@ -3,6 +3,7 @@ package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
@@ -10,12 +11,11 @@ import (
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
// fix this bug, or just stop running subprocesses from within there, given that
// we don't need to see a loading status if we're in a subprocess.
func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess func() error) error {
// TODO: work out if we actually need to use a shell command here
func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
useSubprocess := gui.GitCommand.UsingGpg()
if useSubprocess {
// Need to remember why we use the shell for the subprocess but not in the other case
// Maybe there's no good reason
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.ShellCommandFromString(cmdStr))
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString()))
if success && onSuccess != nil {
if err := onSuccess(); err != nil {
return err
@@ -27,15 +27,16 @@ func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess f
return err
} else {
return gui.RunAndStream(cmdStr, waitingStatus, onSuccess)
return gui.RunAndStream(cmdObj, waitingStatus, onSuccess)
}
}
func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func() error) error {
func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
return gui.WithWaitingStatus(waitingStatus, func() error {
cmd := gui.OSCommand.ShellCommandFromString(cmdStr)
cmd.Env = append(cmd.Env, "TERM=dumb")
cmdObj := gui.OSCommand.Cmd.NewShell(cmdObj.ToString())
cmdObj.AddEnvVars("TERM=dumb")
cmdWriter := gui.getCmdWriter()
cmd := cmdObj.GetCmd()
cmd.Stdout = cmdWriter
cmd.Stderr = cmdWriter
@@ -46,7 +47,7 @@ func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.surfaceError(
fmt.Errorf(
gui.Tr.GitCommandFailed, gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu,
gui.Tr.GitCommandFailed, gui.UserConfig.Keybinding.Universal.ExtrasMenu,
),
)
}
+26 -19
View File
@@ -7,7 +7,6 @@ import (
"os"
"sync"
"os/exec"
"strings"
"time"
@@ -15,6 +14,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
@@ -26,12 +26,10 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
)
@@ -68,8 +66,8 @@ type Repo string
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
*common.Common
g *gocui.Gui
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *oscommands.OSCommand
@@ -80,7 +78,6 @@ type Gui struct {
// gui state when returning from a subrepo
RepoStateMap map[Repo]*guiState
Config config.AppConfigurer
Tr *i18n.TranslationSet
Updater *updates.Updater
statusManager *statusManager
credentials credentials
@@ -121,6 +118,10 @@ type Gui struct {
ShowExtrasWindow bool
suggestionsAsyncHandler *tasks.AsyncHandler
PopupHandler PopupHandler
IsNewRepo bool
}
type listPanelState struct {
@@ -372,7 +373,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
}
}
showTree := gui.Config.GetUserConfig().Gui.ShowFileTree
showTree := gui.UserConfig.Gui.ShowFileTree
contexts := gui.contextTree()
@@ -430,13 +431,13 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
// for now the split view will always be on
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
func NewGui(cmn *common.Common, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
gui := &Gui{
Log: log,
Common: cmn,
GitCommand: gitCommand,
OSCommand: oSCommand,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
@@ -444,8 +445,12 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
RepoPathStack: []string{},
RepoStateMap: map[Repo]*guiState{},
CmdLog: []string{},
ShowExtrasWindow: config.ShowCommandLogOnStartup(),
suggestionsAsyncHandler: tasks.NewAsyncHandler(),
// originally we could only hide the command log permanently via the config
// but now we do it via state. So we need to still support the config for the
// sake of backwards compatibility. We're making use of short circuiting here
ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog,
}
gui.resetState(filterPath, false)
@@ -455,8 +460,9 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
onRunCommand := gui.GetOnRunCommand()
oSCommand.SetOnRunCommand(onRunCommand)
gui.OnRunCommand = onRunCommand
gui.PopupHandler = &RealPopupHandler{gui: gui}
authors.SetCustomAuthors(gui.Config.GetUserConfig().Gui.AuthorColors)
authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors)
return gui, nil
}
@@ -506,7 +512,7 @@ func (gui *Gui) Run() error {
if err := gui.Config.ReloadUserConfig(); err != nil {
return nil
}
userConfig := gui.Config.GetUserConfig()
userConfig := gui.UserConfig
g.SearchEscapeKey = gui.getKey(userConfig.Keybinding.Universal.Return)
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
g.PrevSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.PrevMatch)
@@ -522,7 +528,7 @@ func (gui *Gui) Run() error {
}
gui.waitForIntro.Add(1)
if gui.Config.GetUserConfig().Git.AutoFetch {
if gui.UserConfig.Git.AutoFetch {
go utils.Safe(gui.startBackgroundFetch)
}
@@ -575,7 +581,7 @@ func (gui *Gui) RunAndHandleError() error {
}
// returns whether command exited without error or not
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error {
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdObj) error {
_, err := gui.runSubprocessWithSuspense(subprocess)
if err != nil {
return err
@@ -589,7 +595,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error
}
// returns whether command exited without error or not
func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) {
func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, error) {
gui.Mutexes.SubprocessMutex.Lock()
defer gui.Mutexes.SubprocessMutex.Unlock()
@@ -618,7 +624,8 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) {
return cmdErr == nil, gui.surfaceError(cmdErr)
}
func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error {
subprocess := cmdObj.GetCmd()
subprocess.Stdout = os.Stdout
subprocess.Stderr = os.Stdout
subprocess.Stdin = os.Stdin
@@ -707,8 +714,8 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun
func (gui *Gui) startBackgroundFetch() {
gui.waitForIntro.Wait()
isNew := gui.Config.GetIsNewRepo()
userConfig := gui.Config.GetUserConfig()
isNew := gui.IsNewRepo
userConfig := gui.UserConfig
if !isNew {
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second)
}
@@ -729,7 +736,7 @@ func (gui *Gui) startBackgroundFetch() {
// setColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) setColorScheme() error {
userConfig := gui.Config.GetUserConfig()
userConfig := gui.UserConfig
theme.UpdateTheme(userConfig.Gui.Theme)
gui.g.FgColor = theme.InactiveBorderColor
+20 -1
View File
@@ -204,7 +204,7 @@ func (gui *Gui) getKey(key string) interface{} {
// GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding {
config := gui.Config.GetUserConfig().Keybinding
config := gui.UserConfig.Keybinding
bindings := []*Binding{
{
@@ -901,6 +901,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleCopySelectedCommitMessageToClipboard,
Description: gui.Tr.LcCopyCommitMessageToClipboard,
},
{
ViewName: "commits",
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
Key: gui.getKey(config.Commits.OpenInBrowser),
Handler: gui.handleOpenCommitInBrowser,
Description: gui.Tr.LcOpenCommitInBrowser,
},
{
ViewName: "commits",
Contexts: []string{string(REFLOG_COMMITS_CONTEXT_KEY)},
@@ -1770,6 +1777,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.toggleWhitespaceInDiffView,
Description: gui.Tr.ToggleWhitespaceInDiffView,
},
{
ViewName: "",
Key: gui.getKey(config.Universal.IncreaseContextInDiffView),
Handler: gui.IncreaseContextInDiffView,
Description: gui.Tr.IncreaseContextInDiffView,
},
{
ViewName: "",
Key: gui.getKey(config.Universal.DecreaseContextInDiffView),
Handler: gui.DecreaseContextInDiffView,
Description: gui.Tr.DecreaseContextInDiffView,
},
{
ViewName: "extras",
Key: gocui.MouseWheelUp,
+1 -1
View File
@@ -374,7 +374,7 @@ func (gui *Gui) onInitialViewsCreation() error {
return err
}
if !gui.Config.GetUserConfig().DisableStartupPopups {
if !gui.UserConfig.DisableStartupPopups {
popupTasks := []func(chan struct{}) error{}
storedPopupVersion := gui.Config.GetAppState().StartupPopupVersion
if storedPopupVersion < StartupPopupVersion {
+22 -9
View File
@@ -9,7 +9,8 @@ import (
type ListContext struct {
GetItemsLength func() int
GetDisplayStrings func(startIdx int, length int) [][]string
OnFocus func() error
OnFocus func(...OnFocusOpts) error
OnRenderToMain func(...OnFocusOpts) error
OnFocusLost func() error
OnClickSelectedItem func() error
@@ -29,7 +30,6 @@ type ListContext struct {
type IListContext interface {
GetSelectedItem() (ListItem, bool)
GetSelectedItemId() string
OnRender() error
handlePrevLine() error
handleNextLine() error
handleScrollLeft() error
@@ -42,6 +42,7 @@ type IListContext interface {
handleClick() error
onSearchSelect(selectedLineIdx int) error
FocusLine()
HandleRenderToMain() error
GetPanelState() IListPanelState
@@ -101,7 +102,7 @@ func (self *ListContext) GetSelectedItemId() string {
}
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
func (self *ListContext) OnRender() error {
func (self *ListContext) HandleRender() error {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
@@ -131,7 +132,7 @@ func (self *ListContext) HandleFocusLost() error {
return nil
}
func (self *ListContext) HandleFocus() error {
func (self *ListContext) HandleFocus(opts ...OnFocusOpts) error {
if self.Gui.popupPanelFocused() {
return nil
}
@@ -143,16 +144,20 @@ func (self *ListContext) HandleFocus() error {
}
if self.OnFocus != nil {
return self.OnFocus()
if err := self.OnFocus(opts...); err != nil {
return err
}
}
if self.OnRenderToMain != nil {
if err := self.OnRenderToMain(opts...); err != nil {
return err
}
}
return nil
}
func (self *ListContext) HandleRender() error {
return self.OnRender()
}
func (self *ListContext) handlePrevLine() error {
return self.handleLineChange(-1)
}
@@ -268,3 +273,11 @@ func (self *ListContext) onSearchSelect(selectedLineIdx int) error {
self.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
return self.HandleFocus()
}
func (self *ListContext) HandleRenderToMain() error {
if self.OnRenderToMain != nil {
return self.OnRenderToMain()
}
return nil
}
+22 -20
View File
@@ -18,7 +18,6 @@ func (gui *Gui) menuListContext() IListContext {
},
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: gui.onMenuPress,
Gui: gui,
@@ -36,7 +35,8 @@ func (gui *Gui) filesListContext() IListContext {
},
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnFocus: gui.focusAndSelectFile,
OnFocus: OnFocusWrapper(gui.onFocusFile),
OnRenderToMain: OnFocusWrapper(gui.filesRenderToMain),
OnClickSelectedItem: gui.handleFilePress,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
@@ -65,7 +65,7 @@ func (gui *Gui) branchesListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.Branches) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
OnFocus: gui.handleBranchSelect,
OnRenderToMain: OnFocusWrapper(gui.branchesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
@@ -87,7 +87,7 @@ func (gui *Gui) remotesListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.Remotes) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
OnFocus: gui.handleRemoteSelect,
OnRenderToMain: OnFocusWrapper(gui.remotesRenderToMain),
OnClickSelectedItem: gui.handleRemoteEnter,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
@@ -110,7 +110,7 @@ func (gui *Gui) remoteBranchesListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
OnFocus: gui.handleRemoteBranchSelect,
OnRenderToMain: OnFocusWrapper(gui.remoteBranchesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
@@ -132,7 +132,7 @@ func (gui *Gui) tagsListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.Tags) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
OnFocus: gui.handleTagSelect,
OnRenderToMain: OnFocusWrapper(gui.tagsRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
@@ -145,7 +145,7 @@ func (gui *Gui) tagsListContext() IListContext {
}
func (gui *Gui) branchCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
parseEmoji := gui.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
@@ -155,7 +155,8 @@ func (gui *Gui) branchCommitsListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.Commits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
OnFocus: gui.handleCommitSelect,
OnFocus: OnFocusWrapper(gui.onCommitFocus),
OnRenderToMain: OnFocusWrapper(gui.branchCommitsRenderToMain),
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
@@ -187,7 +188,7 @@ func (gui *Gui) branchCommitsListContext() IListContext {
}
func (gui *Gui) subCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
parseEmoji := gui.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
@@ -197,7 +198,7 @@ func (gui *Gui) subCommitsListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.SubCommits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
OnFocus: gui.handleSubCommitSelect,
OnRenderToMain: OnFocusWrapper(gui.subCommitsRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
selectedCommitSha := ""
@@ -228,7 +229,7 @@ func (gui *Gui) subCommitsListContext() IListContext {
}
func (gui *Gui) shouldShowGraph() bool {
value := gui.Config.GetUserConfig().Git.Log.ShowGraph
value := gui.UserConfig.Git.Log.ShowGraph
switch value {
case "always":
return true
@@ -243,7 +244,7 @@ func (gui *Gui) shouldShowGraph() bool {
}
func (gui *Gui) reflogCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
parseEmoji := gui.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
@@ -253,7 +254,7 @@ func (gui *Gui) reflogCommitsListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
OnFocus: gui.handleReflogCommitSelect,
OnRenderToMain: OnFocusWrapper(gui.reflogCommitsRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetReflogCommitListDisplayStrings(
@@ -281,7 +282,7 @@ func (gui *Gui) stashListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.StashEntries) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
OnFocus: gui.handleStashEntrySelect,
OnRenderToMain: OnFocusWrapper(gui.stashRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
@@ -303,7 +304,8 @@ func (gui *Gui) commitFilesListContext() IListContext {
},
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
OnFocus: gui.handleCommitFileSelect,
OnFocus: OnFocusWrapper(gui.onCommitFileFocus),
OnRenderToMain: OnFocusWrapper(gui.commitFilesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
if gui.State.CommitFileManager.GetItemsLength() == 0 {
@@ -335,7 +337,7 @@ func (gui *Gui) submodulesListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.Submodules) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
OnFocus: gui.handleSubmoduleSelect,
OnRenderToMain: OnFocusWrapper(gui.submodulesRenderToMain),
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
@@ -357,7 +359,6 @@ func (gui *Gui) suggestionsListContext() IListContext {
},
GetItemsLength: func() int { return len(gui.State.Suggestions) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnFocus: func() error { return nil },
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
@@ -386,7 +387,7 @@ func (gui *Gui) getListContexts() []IListContext {
func (gui *Gui) getListContextKeyBindings() []*Binding {
bindings := make([]*Binding, 0)
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
for _, listContext := range gui.getListContexts() {
listContext := listContext
@@ -406,10 +407,11 @@ func (gui *Gui) getListContextKeyBindings() []*Binding {
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.ScrollRight), Modifier: gocui.ModNone, Handler: listContext.handleScrollRight},
}...)
// the commits panel needs to lazyload things so it has a couple of its own handlers
openSearchHandler := gui.handleOpenSearch
gotoBottomHandler := listContext.handleGotoBottom
if listContext.GetViewName() == "commits" {
// the branch commits context needs to lazyload things so it has a couple of its own handlers
if listContext.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
openSearchHandler = gui.handleOpenSearchForCommitsPanel
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
}
+2 -12
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -27,16 +26,10 @@ func (i *menuItem) ID() string {
return strings.Join(i.displayStrings, "-")
}
// list panel functions
func (gui *Gui) handleMenuSelect() error {
return nil
}
// specific functions
func (gui *Gui) getMenuOptions() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
return map[string]string{
gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.Tr.LcClose,
@@ -95,10 +88,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
menuView.SetContent(list)
gui.State.Panels.Menu.SelectedLineIdx = 0
gui.g.Update(func(g *gocui.Gui) error {
return gui.pushContext(gui.State.Contexts.Menu)
})
return nil
return gui.pushContext(gui.State.Contexts.Menu)
}
func (gui *Gui) onMenuPress() error {
+3 -3
View File
@@ -9,8 +9,8 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
)
@@ -227,7 +227,7 @@ func (gui *Gui) centerYPos(view *gocui.View, y int) {
}
func (gui *Gui) getMergingOptions() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
return map[string]string{
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcSelectHunk,
@@ -262,7 +262,7 @@ func (gui *Gui) handleCompleteMerge() error {
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_NORMAL {
if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_NONE {
return gui.handleEscapeMerge()
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
+2 -2
View File
@@ -1,7 +1,7 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
@@ -61,7 +61,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
},
{
isActive: func() bool {
return gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL
return gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE
},
description: func() string {
workingTreeState := gui.GitCommand.WorkingTreeState()
+3 -3
View File
@@ -3,7 +3,7 @@ package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
func (gui *Gui) handleCreatePatchOptionsMenu() error {
@@ -26,7 +26,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
},
}
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == commands.REBASE_MODE_NORMAL {
if gui.GitCommand.PatchManager.CanRebase && gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_NONE {
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.To),
@@ -74,7 +74,7 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL {
if gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE {
return false, gui.createErrorPanel(gui.Tr.CantPatchWhileRebasingError)
}
return true, nil
+87
View File
@@ -0,0 +1,87 @@
package gui
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
type PopupHandler interface {
Error(message string) error
Ask(opts askOpts) error
Prompt(opts promptOpts) error
Loader(message string) error
}
type RealPopupHandler struct {
gui *Gui
}
func (self *RealPopupHandler) Error(message string) error {
gui := self.gui
coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message))
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
return self.Ask(askOpts{
title: gui.Tr.Error,
prompt: coloredMessage,
})
}
func (self *RealPopupHandler) Ask(opts askOpts) error {
gui := self.gui
return gui.createPopupPanel(createPopupPanelOpts{
title: opts.title,
prompt: opts.prompt,
handleConfirm: opts.handleConfirm,
handleClose: opts.handleClose,
handlersManageFocus: opts.handlersManageFocus,
})
}
func (self *RealPopupHandler) Prompt(opts promptOpts) error {
gui := self.gui
return gui.createPopupPanel(createPopupPanelOpts{
title: opts.title,
prompt: opts.initialContent,
editable: true,
handleConfirmPrompt: opts.handleConfirm,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
}
func (self *RealPopupHandler) Loader(message string) error {
gui := self.gui
return gui.createPopupPanel(createPopupPanelOpts{
prompt: message,
hasLoader: true,
})
}
type TestPopupHandler struct {
onError func(message string) error
onAsk func(opts askOpts) error
onPrompt func(opts promptOpts) error
}
func (self *TestPopupHandler) Error(message string) error {
return self.onError(message)
}
func (self *TestPopupHandler) Ask(opts askOpts) error {
return self.onAsk(opts)
}
func (self *TestPopupHandler) Prompt(opts promptOpts) error {
return self.onPrompt(opts)
}
func (self *TestPopupHandler) Loader(message string) error {
return nil
}
+12 -3
View File
@@ -13,9 +13,13 @@ import (
// if these being global variables causes trouble we can wrap them in a struct
// attached to the gui state.
var authorInitialCache = make(map[string]string)
var authorNameCache = make(map[string]string)
var authorStyleCache = make(map[string]style.TextStyle)
var (
authorInitialCache = make(map[string]string)
authorNameCache = make(map[string]string)
authorStyleCache = make(map[string]style.TextStyle)
)
const authorNameWildcard = "*"
func ShortAuthor(authorName string) string {
if value, ok := authorInitialCache[authorName]; ok {
@@ -51,6 +55,11 @@ func AuthorStyle(authorName string) style.TextStyle {
return value
}
// use the unified style whatever the author name is
if value, ok := authorStyleCache[authorNameWildcard]; ok {
return value
}
value := trueColorStyle(authorName)
authorStyleCache[authorName] = value
+14 -3
View File
@@ -3,7 +3,7 @@ package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
@@ -56,12 +56,23 @@ func (gui *Gui) createPullRequestMenu(selectedBranch *models.Branch, checkedOutB
}
func (gui *Gui) createPullRequest(from string, to string) error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
url, err := pullRequest.Create(from, to)
hostingServiceMgr := gui.getHostingServiceMgr()
url, err := hostingServiceMgr.GetPullRequestURL(from, to)
if err != nil {
return gui.surfaceError(err)
}
if err := gui.GitCommand.OSCommand.OpenLink(url); err != nil {
return gui.surfaceError(err)
}
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.CreatingPullRequestAtUrl, url), gui.Tr.CreatePullRequest, false))
return nil
}
func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
remoteUrl := gui.GitCommand.GetRemoteURL()
configServices := gui.UserConfig.Services
return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices)
}
+2 -2
View File
@@ -60,7 +60,7 @@ func (gui *Gui) handleTopLevelReturn() error {
return gui.dispatchSwitchToRepo(path, true)
}
if gui.Config.GetUserConfig().QuitOnTopLevelReturn {
if gui.UserConfig.QuitOnTopLevelReturn {
return gui.handleQuit()
}
@@ -72,7 +72,7 @@ func (gui *Gui) quit() error {
return gui.createUpdateQuitConfirmation()
}
if gui.Config.GetUserConfig().ConfirmOnQuit {
if gui.UserConfig.ConfirmOnQuit {
return gui.ask(askOpts{
title: "",
prompt: gui.Tr.ConfirmQuit,

Some files were not shown because too many files have changed in this diff Show More