mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-10 20:05:50 +02:00
add support for git bisect
This commit is contained in:
parent
ab84410b41
commit
4ab5e54139
117 changed files with 1013 additions and 104 deletions
|
@ -205,6 +205,7 @@ keybinding:
|
|||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
openLogMenu: '<c-l>'
|
||||
viewBisectOptions: 'b'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
|
@ -389,6 +390,7 @@ gui:
|
|||
```
|
||||
|
||||
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:
|
||||
|
|
|
@ -69,6 +69,7 @@ For a given custom command, here are the allowed fields:
|
|||
| prompts | a list of prompts that will request user input before running the final command | no |
|
||||
| loadingText | text to display while waiting for command to finish | no |
|
||||
| description | text to display in the keybindings menu that appears when you press 'x' | no |
|
||||
| stream | whether you want to stream the command's output to the Command Log panel | no |
|
||||
|
||||
### Contexts
|
||||
|
||||
|
|
|
@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Reflog Tab)
|
||||
|
|
|
@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Reflog Tabblad)
|
||||
|
|
|
@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Reflog Tab)
|
||||
|
|
|
@ -149,6 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
|||
<kbd>ctrl+r</kbd>: 重置已拣选(复制)的提交
|
||||
<kbd>ctrl+y</kbd>: 将提交消息复制到剪贴板
|
||||
<kbd>o</kbd>: open commit in browser
|
||||
<kbd>b</kbd>: view bisect options
|
||||
</pre>
|
||||
|
||||
## 提交 面板 (Reflog)
|
||||
|
|
|
@ -36,6 +36,7 @@ type GitCommand struct {
|
|||
Sync *git_commands.SyncCommands
|
||||
Tag *git_commands.TagCommands
|
||||
WorkingTree *git_commands.WorkingTreeCommands
|
||||
Bisect *git_commands.BisectCommands
|
||||
|
||||
Loaders Loaders
|
||||
}
|
||||
|
@ -113,6 +114,7 @@ func NewGitCommandAux(
|
|||
// TODO: have patch manager take workingTreeCommands in its entirety
|
||||
patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
|
||||
return &GitCommand{
|
||||
Branch: branchCommands,
|
||||
|
@ -129,6 +131,7 @@ func NewGitCommandAux(
|
|||
Submodule: submoduleCommands,
|
||||
Sync: syncCommands,
|
||||
Tag: tagCommands,
|
||||
Bisect: bisectCommands,
|
||||
WorkingTree: workingTreeCommands,
|
||||
Loaders: Loaders{
|
||||
Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName, configCommands),
|
||||
|
|
164
pkg/commands/git_commands/bisect.go
Normal file
164
pkg/commands/git_commands/bisect.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type BisectCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
func NewBisectCommands(gitCommon *GitCommon) *BisectCommands {
|
||||
return &BisectCommands{
|
||||
GitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
|
||||
// This command is pretty cheap to run so we're not storing the result anywhere.
|
||||
// But if it becomes problematic we can chang that.
|
||||
func (self *BisectCommands) GetInfo() *BisectInfo {
|
||||
var err error
|
||||
info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"}
|
||||
// we return nil if we're not in a git bisect session.
|
||||
// we know we're in a session by the presence of a .git/BISECT_START file
|
||||
|
||||
bisectStartPath := filepath.Join(self.dotGitDir, "BISECT_START")
|
||||
exists, err := self.os.FileExists(bisectStartPath)
|
||||
if err != nil {
|
||||
self.Log.Infof("error getting git bisect info: %s", err.Error())
|
||||
return info
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return info
|
||||
}
|
||||
|
||||
startContent, err := os.ReadFile(bisectStartPath)
|
||||
if err != nil {
|
||||
self.Log.Infof("error getting git bisect info: %s", err.Error())
|
||||
return info
|
||||
}
|
||||
|
||||
info.started = true
|
||||
info.start = strings.TrimSpace(string(startContent))
|
||||
|
||||
termsContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_TERMS"))
|
||||
if err != nil {
|
||||
// old git versions won't have this file so we default to bad/good
|
||||
} else {
|
||||
splitContent := strings.Split(string(termsContent), "\n")
|
||||
info.newTerm = splitContent[0]
|
||||
info.oldTerm = splitContent[1]
|
||||
}
|
||||
|
||||
bisectRefsDir := filepath.Join(self.dotGitDir, "refs", "bisect")
|
||||
files, err := os.ReadDir(bisectRefsDir)
|
||||
if err != nil {
|
||||
self.Log.Infof("error getting git bisect info: %s", err.Error())
|
||||
return info
|
||||
}
|
||||
|
||||
info.statusMap = make(map[string]BisectStatus)
|
||||
for _, file := range files {
|
||||
status := BisectStatusSkipped
|
||||
name := file.Name()
|
||||
path := filepath.Join(bisectRefsDir, name)
|
||||
|
||||
fileContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
self.Log.Infof("error getting git bisect info: %s", err.Error())
|
||||
return info
|
||||
}
|
||||
|
||||
sha := strings.TrimSpace(string(fileContent))
|
||||
|
||||
if name == info.newTerm {
|
||||
status = BisectStatusNew
|
||||
} else if strings.HasPrefix(name, info.oldTerm+"-") {
|
||||
status = BisectStatusOld
|
||||
} else if strings.HasPrefix(name, "skipped-") {
|
||||
status = BisectStatusSkipped
|
||||
}
|
||||
|
||||
info.statusMap[sha] = status
|
||||
}
|
||||
|
||||
currentContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_EXPECTED_REV"))
|
||||
if err != nil {
|
||||
self.Log.Infof("error getting git bisect info: %s", err.Error())
|
||||
return info
|
||||
}
|
||||
currentSha := strings.TrimSpace(string(currentContent))
|
||||
info.current = currentSha
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func (self *BisectCommands) Reset() error {
|
||||
return self.cmd.New("git bisect reset").StreamOutput().Run()
|
||||
}
|
||||
|
||||
func (self *BisectCommands) Mark(ref string, term string) error {
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf("git bisect %s %s", term, ref),
|
||||
).
|
||||
IgnoreEmptyError().
|
||||
StreamOutput().
|
||||
Run()
|
||||
}
|
||||
|
||||
func (self *BisectCommands) Skip(ref string) error {
|
||||
return self.Mark(ref, "skip")
|
||||
}
|
||||
|
||||
func (self *BisectCommands) Start() error {
|
||||
return self.cmd.New("git bisect start").StreamOutput().Run()
|
||||
}
|
||||
|
||||
// tells us whether we've found our problem commit(s). We return a string slice of
|
||||
// commit sha's if we're done, and that slice may have more that one item if
|
||||
// skipped commits are involved.
|
||||
func (self *BisectCommands) IsDone() (bool, []string, error) {
|
||||
info := self.GetInfo()
|
||||
if !info.Bisecting() {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
newSha := info.GetNewSha()
|
||||
if newSha == "" {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
// if we start from the new commit and reach the a good commit without
|
||||
// coming across any unprocessed commits, then we're done
|
||||
done := false
|
||||
candidates := []string{}
|
||||
|
||||
err := self.cmd.New(fmt.Sprintf("git rev-list %s", newSha)).RunAndProcessLines(func(line string) (bool, error) {
|
||||
sha := strings.TrimSpace(line)
|
||||
|
||||
if status, ok := info.statusMap[sha]; ok {
|
||||
switch status {
|
||||
case BisectStatusSkipped, BisectStatusNew:
|
||||
candidates = append(candidates, sha)
|
||||
return false, nil
|
||||
case BisectStatusOld:
|
||||
done = true
|
||||
return true, nil
|
||||
}
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// should never land here
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
return done, candidates, nil
|
||||
}
|
103
pkg/commands/git_commands/bisect_info.go
Normal file
103
pkg/commands/git_commands/bisect_info.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package git_commands
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
// although the typical terms in a git bisect are 'bad' and 'good', they're more
|
||||
// generally known as 'new' and 'old'. Semi-recently git allowed the user to define
|
||||
// their own terms e.g. when you want to used 'fixed', 'unfixed' in the event
|
||||
// that you're looking for a commit that fixed a bug.
|
||||
|
||||
// Git bisect only keeps track of a single 'bad' commit. Once you pick a commit
|
||||
// that's older than the current bad one, it forgets about the previous one. On
|
||||
// the other hand, it does keep track of all the good and skipped commits.
|
||||
|
||||
type BisectInfo struct {
|
||||
log *logrus.Entry
|
||||
|
||||
// tells us whether all our git bisect files are there meaning we're in bisect mode.
|
||||
// Doesn't necessarily mean that we've actually picked a good/bad commit yet.
|
||||
started bool
|
||||
|
||||
// this is the ref you started the commit from
|
||||
start string // this will always be defined
|
||||
|
||||
// these will be defined if we've started
|
||||
newTerm string // 'bad' by default
|
||||
oldTerm string // 'good' by default
|
||||
|
||||
// map of commit sha's to their status
|
||||
statusMap map[string]BisectStatus
|
||||
|
||||
// the sha of the commit that's under test
|
||||
current string
|
||||
}
|
||||
|
||||
type BisectStatus int
|
||||
|
||||
const (
|
||||
BisectStatusOld BisectStatus = iota
|
||||
BisectStatusNew
|
||||
BisectStatusSkipped
|
||||
)
|
||||
|
||||
// null object pattern
|
||||
func NewNullBisectInfo() *BisectInfo {
|
||||
return &BisectInfo{started: false}
|
||||
}
|
||||
|
||||
func (self *BisectInfo) GetNewSha() string {
|
||||
for sha, status := range self.statusMap {
|
||||
if status == BisectStatusNew {
|
||||
return sha
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (self *BisectInfo) GetCurrentSha() string {
|
||||
return self.current
|
||||
}
|
||||
|
||||
func (self *BisectInfo) StartSha() string {
|
||||
return self.start
|
||||
}
|
||||
|
||||
func (self *BisectInfo) Status(commitSha string) (BisectStatus, bool) {
|
||||
status, ok := self.statusMap[commitSha]
|
||||
return status, ok
|
||||
}
|
||||
|
||||
func (self *BisectInfo) NewTerm() string {
|
||||
return self.newTerm
|
||||
}
|
||||
|
||||
func (self *BisectInfo) OldTerm() string {
|
||||
return self.oldTerm
|
||||
}
|
||||
|
||||
// this is for when we have called `git bisect start`. It does not
|
||||
// mean that we have actually started narrowing things down or selecting good/bad commits
|
||||
func (self *BisectInfo) Started() bool {
|
||||
return self.started
|
||||
}
|
||||
|
||||
// this is where we have both a good and bad revision and we're actually
|
||||
// starting to narrow things down
|
||||
func (self *BisectInfo) Bisecting() bool {
|
||||
if !self.Started() {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.GetNewSha() == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, status := range self.statusMap {
|
||||
if status == BisectStatusOld {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -75,7 +75,19 @@ func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
|
|||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error) {
|
||||
return self.cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).DontLog().RunWithOutput()
|
||||
return self.GetCommitMessagesFirstLine([]string{sha})
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, error) {
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", strings.Join(shas, " ")),
|
||||
).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) {
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf("git show --no-patch --oneline %s", strings.Join(shas, " ")),
|
||||
).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package models
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
|
@ -18,10 +22,7 @@ type Commit struct {
|
|||
}
|
||||
|
||||
func (c *Commit) ShortSha() string {
|
||||
if len(c.Sha) < 8 {
|
||||
return c.Sha
|
||||
}
|
||||
return c.Sha[:8]
|
||||
return utils.ShortSha(c.Sha)
|
||||
}
|
||||
|
||||
func (c *Commit) RefName() string {
|
||||
|
|
|
@ -35,6 +35,18 @@ type ICmdObj interface {
|
|||
// This returns false if DontLog() was called
|
||||
ShouldLog() bool
|
||||
|
||||
// when you call this, then call Run(), we'll stream the output to the cmdWriter (i.e. the command log panel)
|
||||
StreamOutput() ICmdObj
|
||||
// returns true if StreamOutput() was called
|
||||
ShouldStreamOutput() bool
|
||||
|
||||
// if you call this before ShouldStreamOutput we'll consider an error with no
|
||||
// stderr content as a non-error. Not yet supported for Run or RunWithOutput (
|
||||
// but adding support is trivial)
|
||||
IgnoreEmptyError() ICmdObj
|
||||
// returns true if IgnoreEmptyError() was called
|
||||
ShouldIgnoreEmptyError() bool
|
||||
|
||||
PromptOnCredentialRequest() ICmdObj
|
||||
FailOnCredentialRequest() ICmdObj
|
||||
|
||||
|
@ -47,9 +59,15 @@ type CmdObj struct {
|
|||
|
||||
runner ICmdObjRunner
|
||||
|
||||
// if set to true, we don't want to log the command to the user.
|
||||
// see DontLog()
|
||||
dontLog bool
|
||||
|
||||
// see StreamOutput()
|
||||
streamOutput bool
|
||||
|
||||
// see IgnoreEmptyError()
|
||||
ignoreEmptyError bool
|
||||
|
||||
// if set to true, it means we might be asked to enter a username/password by this command.
|
||||
credentialStrategy CredentialStrategy
|
||||
}
|
||||
|
@ -98,6 +116,26 @@ func (self *CmdObj) ShouldLog() bool {
|
|||
return !self.dontLog
|
||||
}
|
||||
|
||||
func (self *CmdObj) StreamOutput() ICmdObj {
|
||||
self.streamOutput = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) ShouldStreamOutput() bool {
|
||||
return self.streamOutput
|
||||
}
|
||||
|
||||
func (self *CmdObj) IgnoreEmptyError() ICmdObj {
|
||||
self.ignoreEmptyError = true
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) ShouldIgnoreEmptyError() bool {
|
||||
return self.ignoreEmptyError
|
||||
}
|
||||
|
||||
func (self *CmdObj) Run() error {
|
||||
return self.runner.Run(self)
|
||||
}
|
||||
|
|
|
@ -34,15 +34,27 @@ type cmdObjRunner struct {
|
|||
var _ ICmdObjRunner = &cmdObjRunner{}
|
||||
|
||||
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
if cmdObj.GetCredentialStrategy() == NONE {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
} else {
|
||||
if cmdObj.GetCredentialStrategy() != NONE {
|
||||
return self.runWithCredentialHandling(cmdObj)
|
||||
}
|
||||
|
||||
if cmdObj.ShouldStreamOutput() {
|
||||
return self.runAndStream(cmdObj)
|
||||
}
|
||||
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
if cmdObj.ShouldStreamOutput() {
|
||||
err := self.runAndStream(cmdObj)
|
||||
// for now we're not capturing output, just because it would take a little more
|
||||
// effort and there's currently no use case for it. Some commands call RunWithOutput
|
||||
// but ignore the output, hence why we've got this check here.
|
||||
return "", err
|
||||
}
|
||||
|
||||
if cmdObj.GetCredentialStrategy() != NONE {
|
||||
err := self.runWithCredentialHandling(cmdObj)
|
||||
// for now we're not capturing output, just because it would take a little more
|
||||
|
@ -145,12 +157,36 @@ type cmdHandler struct {
|
|||
close func() error
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStream(cmdObj ICmdObj) error {
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
go func() {
|
||||
_, _ = io.Copy(cmdWriter, handler.stdoutPipe)
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// runAndDetectCredentialRequest detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
func (self *cmdObjRunner) runAndDetectCredentialRequest(
|
||||
cmdObj ICmdObj,
|
||||
promptUserForCredential func(CredentialType) string,
|
||||
) error {
|
||||
// setting the output to english so we can parse it for a username/password request
|
||||
cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
return self.runAndStreamAux(cmdObj, func(handler *cmdHandler, cmdWriter io.Writer) {
|
||||
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) runAndStreamAux(
|
||||
cmdObj ICmdObj,
|
||||
onRun func(*cmdHandler, io.Writer),
|
||||
) error {
|
||||
cmdWriter := self.guiIO.newCmdWriterFn()
|
||||
|
||||
|
@ -158,7 +194,7 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
|
|||
self.logCmdObj(cmdObj)
|
||||
}
|
||||
self.log.WithField("command", cmdObj.ToString()).Info("RunCommand")
|
||||
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
|
||||
cmd := cmdObj.GetCmd()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = io.MultiWriter(cmdWriter, &stderr)
|
||||
|
@ -174,14 +210,14 @@ func (self *cmdObjRunner) runAndDetectCredentialRequest(
|
|||
}
|
||||
}()
|
||||
|
||||
tr := io.TeeReader(handler.stdoutPipe, cmdWriter)
|
||||
|
||||
go utils.Safe(func() {
|
||||
self.processOutput(tr, handler.stdinPipe, promptUserForCredential)
|
||||
})
|
||||
onRun(handler, cmdWriter)
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
errStr := stderr.String()
|
||||
if cmdObj.ShouldIgnoreEmptyError() && errStr == "" {
|
||||
return nil
|
||||
}
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
|
|
|
@ -247,6 +247,7 @@ type KeybindingCommitsConfig struct {
|
|||
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
|
||||
OpenLogMenu string `yaml:"openLogMenu"`
|
||||
OpenInBrowser string `yaml:"openInBrowser"`
|
||||
ViewBisectOptions string `yaml:"viewBisectOptions"`
|
||||
}
|
||||
|
||||
type KeybindingStashConfig struct {
|
||||
|
@ -293,6 +294,7 @@ type CustomCommand struct {
|
|||
Prompts []CustomCommandPrompt `yaml:"prompts"`
|
||||
LoadingText string `yaml:"loadingText"`
|
||||
Description string `yaml:"description"`
|
||||
Stream bool `yaml:"stream"`
|
||||
}
|
||||
|
||||
type CustomCommandPrompt struct {
|
||||
|
@ -510,6 +512,7 @@ func GetDefaultConfig() *UserConfig {
|
|||
CopyCommitMessageToClipboard: "<c-y>",
|
||||
OpenLogMenu: "<c-l>",
|
||||
OpenInBrowser: "o",
|
||||
ViewBisectOptions: "b",
|
||||
},
|
||||
Stash: KeybindingStashConfig{
|
||||
PopStash: "g",
|
||||
|
|
204
pkg/gui/bisect.go
Normal file
204
pkg/gui/bisect.go
Normal file
|
@ -0,0 +1,204 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleOpenBisectMenu() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// no shame in getting this directly rather than using the cached value
|
||||
// given how cheap it is to obtain
|
||||
info := gui.Git.Bisect.GetInfo()
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if info.Started() {
|
||||
return gui.openMidBisectMenu(info, commit)
|
||||
} else {
|
||||
return gui.openStartBisectMenu(info, commit)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) openMidBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
||||
// if there is not yet a 'current' bisect commit, or if we have
|
||||
// selected the current commit, we need to jump to the next 'current' commit
|
||||
// after we perform a bisect action. The reason we don't unconditionally jump
|
||||
// is that sometimes the user will want to go and mark a few commits as skipped
|
||||
// in a row and they wouldn't want to be jumped back to the current bisect
|
||||
// commit each time.
|
||||
// Originally we were allowing the user to, from the bisect menu, select whether
|
||||
// they were talking about the selected commit or the current bisect commit,
|
||||
// and that was a bit confusing (and required extra keypresses).
|
||||
selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha
|
||||
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.BisectMark)
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.afterMark(selectCurrentAfter)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.BisectMark)
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.afterMark(selectCurrentAfter)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.Skip, commit.ShortSha()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.BisectSkip)
|
||||
if err := gui.Git.Bisect.Skip(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.afterMark(selectCurrentAfter)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.Bisect.ResetOption,
|
||||
onPress: func() error {
|
||||
return gui.resetBisect()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(
|
||||
gui.Tr.Bisect.BisectMenuTitle,
|
||||
menuItems,
|
||||
createMenuOptions{showCancel: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) openStartBisectMenu(info *git_commands.BisectInfo, commit *models.Commit) error {
|
||||
return gui.createMenu(
|
||||
gui.Tr.Bisect.BisectMenuTitle,
|
||||
[]*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.StartBisect)
|
||||
if err := gui.Git.Bisect.Start(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: fmt.Sprintf(gui.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()),
|
||||
onPress: func() error {
|
||||
gui.logAction(gui.Tr.Actions.StartBisect)
|
||||
if err := gui.Git.Bisect.Start(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.Git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
},
|
||||
},
|
||||
createMenuOptions{showCancel: true},
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) resetBisect() error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.Bisect.ResetTitle,
|
||||
prompt: gui.Tr.Bisect.ResetPrompt,
|
||||
handleConfirm: func() error {
|
||||
gui.logAction(gui.Tr.Actions.ResetBisect)
|
||||
if err := gui.Git.Bisect.Reset(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) showBisectCompleteMessage(candidateShas []string) error {
|
||||
prompt := gui.Tr.Bisect.CompletePrompt
|
||||
if len(candidateShas) > 1 {
|
||||
prompt = gui.Tr.Bisect.CompletePromptIndeterminate
|
||||
}
|
||||
|
||||
formattedCommits, err := gui.Git.Commit.GetCommitsOneline(candidateShas)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.Bisect.CompleteTitle,
|
||||
prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)),
|
||||
handleConfirm: func() error {
|
||||
gui.logAction(gui.Tr.Actions.ResetBisect)
|
||||
if err := gui.Git.Bisect.Reset(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.postBisectCommandRefresh()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) afterMark(selectCurrent bool) error {
|
||||
done, candidateShas, err := gui.Git.Bisect.IsDone()
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.postBisectCommandRefresh(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if done {
|
||||
return gui.showBisectCompleteMessage(candidateShas)
|
||||
}
|
||||
|
||||
if selectCurrent {
|
||||
gui.selectCurrentBisectCommit()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) selectCurrentBisectCommit() {
|
||||
info := gui.Git.Bisect.GetInfo()
|
||||
if info.GetCurrentSha() != "" {
|
||||
// find index of commit with that sha, move cursor to that.
|
||||
for i, commit := range gui.State.Commits {
|
||||
if commit.Sha == info.GetCurrentSha() {
|
||||
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(i)
|
||||
_ = gui.State.Contexts.BranchCommits.HandleFocus()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) postBisectCommandRefresh() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{}})
|
||||
}
|
|
@ -116,12 +116,19 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
|||
gui.Mutexes.BranchCommitsMutex.Lock()
|
||||
defer gui.Mutexes.BranchCommitsMutex.Unlock()
|
||||
|
||||
refName := "HEAD"
|
||||
bisectInfo := gui.Git.Bisect.GetInfo()
|
||||
gui.State.BisectInfo = bisectInfo
|
||||
if bisectInfo.Started() {
|
||||
refName = bisectInfo.StartSha()
|
||||
}
|
||||
|
||||
commits, err := gui.Git.Loaders.Commits.GetCommits(
|
||||
loaders.GetCommitsOptions{
|
||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||
IncludeRebaseCommits: true,
|
||||
RefName: "HEAD",
|
||||
RefName: refName,
|
||||
All: gui.State.ShowWholeGitGraph,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -254,7 +254,11 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
|||
}
|
||||
return gui.WithWaitingStatus(loadingText, func() error {
|
||||
gui.logAction(gui.Tr.Actions.CustomCommand)
|
||||
err := gui.OSCommand.Cmd.NewShell(cmdStr).Run()
|
||||
cmdObj := gui.OSCommand.Cmd.NewShell(cmdStr)
|
||||
if customCommand.Stream {
|
||||
cmdObj.StreamOutput()
|
||||
}
|
||||
err := cmdObj.Run()
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package filetree
|
|||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -114,6 +113,6 @@ func (m *CommitFileManager) Render(diffName string, patchManager *patch.PatchMan
|
|||
status = patch.PART
|
||||
}
|
||||
|
||||
return presentation.GetCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
return getCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -136,6 +135,6 @@ func (m *FileManager) Render(diffName string, submoduleConfigs []*models.Submodu
|
|||
|
||||
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
|
||||
castN := n.(*FileNode)
|
||||
return presentation.GetFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
return getFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
package presentation
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string {
|
||||
// TODO: move this back into presentation package and fix the import cycle
|
||||
|
||||
func getFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
partiallyModifiedColor := style.FgYellow
|
||||
|
@ -51,3 +54,43 @@ func GetFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, di
|
|||
|
||||
return output
|
||||
}
|
||||
|
||||
func getCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string {
|
||||
var colour style.TextStyle
|
||||
if diffName == name {
|
||||
colour = theme.DiffTerminalColor
|
||||
} else {
|
||||
switch status {
|
||||
case patch.WHOLE:
|
||||
colour = style.FgGreen
|
||||
case patch.PART:
|
||||
colour = style.FgYellow
|
||||
case patch.UNSELECTED:
|
||||
colour = theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
name = utils.EscapeSpecialChars(name)
|
||||
if commitFile == nil {
|
||||
return colour.Sprint(name)
|
||||
}
|
||||
|
||||
return getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " " + colour.Sprint(name)
|
||||
}
|
||||
|
||||
func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
switch changeStatus {
|
||||
case "A":
|
||||
return style.FgGreen
|
||||
case "M", "R":
|
||||
return style.FgYellow
|
||||
case "D":
|
||||
return style.FgRed
|
||||
case "C":
|
||||
return style.FgCyan
|
||||
case "T":
|
||||
return style.FgMagenta
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
|
@ -309,6 +310,7 @@ type guiState struct {
|
|||
RemoteBranches []*models.RemoteBranch
|
||||
Tags []*models.Tag
|
||||
MenuItems []*menuItem
|
||||
BisectInfo *git_commands.BisectInfo
|
||||
Updating bool
|
||||
Panels *panelStates
|
||||
SplitMainPanel bool
|
||||
|
@ -394,6 +396,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
|||
FilteredReflogCommits: make([]*models.Commit, 0),
|
||||
ReflogCommits: make([]*models.Commit, 0),
|
||||
StashEntries: make([]*models.StashEntry, 0),
|
||||
BisectInfo: gui.Git.Bisect.GetInfo(),
|
||||
Panels: &panelStates{
|
||||
// TODO: work out why some of these are -1 and some are 0. Last time I checked there was a good reason but I'm less certain now
|
||||
Files: &filePanelState{listPanelState{SelectedLineIdx: -1}},
|
||||
|
|
|
@ -908,6 +908,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
|||
Handler: gui.handleOpenCommitInBrowser,
|
||||
Description: gui.Tr.LcOpenCommitInBrowser,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Commits.ViewBisectOptions),
|
||||
Handler: gui.handleOpenBisectMenu,
|
||||
Description: gui.Tr.LcViewBisectOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(REFLOG_COMMITS_CONTEXT_KEY)},
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"log"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
@ -177,6 +178,7 @@ func (gui *Gui) branchCommitsListContext() IListContext {
|
|||
startIdx,
|
||||
length,
|
||||
gui.shouldShowGraph(),
|
||||
gui.State.BisectInfo,
|
||||
)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
|
@ -218,6 +220,7 @@ func (gui *Gui) subCommitsListContext() IListContext {
|
|||
startIdx,
|
||||
length,
|
||||
gui.shouldShowGraph(),
|
||||
git_commands.NewNullBisectInfo(),
|
||||
)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
@ -16,11 +18,13 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
|||
{
|
||||
isActive: gui.State.Modes.Diffing.Active,
|
||||
description: func() string {
|
||||
return style.FgMagenta.Sprintf(
|
||||
"%s %s %s",
|
||||
gui.Tr.LcShowingGitDiff,
|
||||
"git diff "+gui.diffStr(),
|
||||
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
|
||||
return gui.withResetButton(
|
||||
fmt.Sprintf(
|
||||
"%s %s",
|
||||
gui.Tr.LcShowingGitDiff,
|
||||
"git diff "+gui.diffStr(),
|
||||
),
|
||||
style.FgMagenta,
|
||||
)
|
||||
},
|
||||
reset: gui.exitDiffMode,
|
||||
|
@ -28,22 +32,20 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
|||
{
|
||||
isActive: gui.Git.Patch.PatchManager.Active,
|
||||
description: func() string {
|
||||
return style.FgYellow.SetBold().Sprintf(
|
||||
"%s %s",
|
||||
gui.Tr.LcBuildingPatch,
|
||||
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
|
||||
)
|
||||
return gui.withResetButton(gui.Tr.LcBuildingPatch, style.FgYellow.SetBold())
|
||||
},
|
||||
reset: gui.handleResetPatch,
|
||||
},
|
||||
{
|
||||
isActive: gui.State.Modes.Filtering.Active,
|
||||
description: func() string {
|
||||
return style.FgRed.SetBold().Sprintf(
|
||||
"%s '%s' %s",
|
||||
gui.Tr.LcFilteringBy,
|
||||
gui.State.Modes.Filtering.GetPath(),
|
||||
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
|
||||
return gui.withResetButton(
|
||||
fmt.Sprintf(
|
||||
"%s '%s'",
|
||||
gui.Tr.LcFilteringBy,
|
||||
gui.State.Modes.Filtering.GetPath(),
|
||||
),
|
||||
style.FgRed,
|
||||
)
|
||||
},
|
||||
reset: gui.exitFilterMode,
|
||||
|
@ -51,10 +53,12 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
|||
{
|
||||
isActive: gui.State.Modes.CherryPicking.Active,
|
||||
description: func() string {
|
||||
return style.FgCyan.Sprintf(
|
||||
"%d commits copied %s",
|
||||
len(gui.State.Modes.CherryPicking.CherryPickedCommits),
|
||||
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
|
||||
return gui.withResetButton(
|
||||
fmt.Sprintf(
|
||||
"%d commits copied",
|
||||
len(gui.State.Modes.CherryPicking.CherryPickedCommits),
|
||||
),
|
||||
style.FgCyan,
|
||||
)
|
||||
},
|
||||
reset: gui.exitCherryPickingMode,
|
||||
|
@ -65,13 +69,28 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
|||
},
|
||||
description: func() string {
|
||||
workingTreeState := gui.Git.Status.WorkingTreeState()
|
||||
return style.FgYellow.Sprintf(
|
||||
"%s %s",
|
||||
formatWorkingTreeState(workingTreeState),
|
||||
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
|
||||
return gui.withResetButton(
|
||||
formatWorkingTreeState(workingTreeState), style.FgYellow,
|
||||
)
|
||||
},
|
||||
reset: gui.abortMergeOrRebaseWithConfirm,
|
||||
},
|
||||
{
|
||||
isActive: func() bool {
|
||||
return gui.State.BisectInfo.Started()
|
||||
},
|
||||
description: func() string {
|
||||
return gui.withResetButton("bisecting", style.FgGreen)
|
||||
},
|
||||
reset: gui.resetBisect,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) withResetButton(content string, textStyle style.TextStyle) string {
|
||||
return textStyle.Sprintf(
|
||||
"%s %s",
|
||||
content,
|
||||
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string {
|
||||
var colour style.TextStyle
|
||||
if diffName == name {
|
||||
colour = theme.DiffTerminalColor
|
||||
} else {
|
||||
switch status {
|
||||
case patch.WHOLE:
|
||||
colour = style.FgGreen
|
||||
case patch.PART:
|
||||
colour = style.FgYellow
|
||||
case patch.UNSELECTED:
|
||||
colour = theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
name = utils.EscapeSpecialChars(name)
|
||||
if commitFile == nil {
|
||||
return colour.Sprint(name)
|
||||
}
|
||||
|
||||
return getColorForChangeStatus(commitFile.ChangeStatus).Sprint(commitFile.ChangeStatus) + " " + colour.Sprint(name)
|
||||
}
|
||||
|
||||
func getColorForChangeStatus(changeStatus string) style.TextStyle {
|
||||
switch changeStatus {
|
||||
case "A":
|
||||
return style.FgGreen
|
||||
case "M", "R":
|
||||
return style.FgYellow
|
||||
case "D":
|
||||
return style.FgRed
|
||||
case "C":
|
||||
return style.FgCyan
|
||||
case "T":
|
||||
return style.FgMagenta
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
|
||||
|
@ -21,6 +22,14 @@ type pipeSetCacheKey struct {
|
|||
var pipeSetCache = make(map[pipeSetCacheKey][][]*graph.Pipe)
|
||||
var mutex sync.Mutex
|
||||
|
||||
type BisectProgress int
|
||||
|
||||
const (
|
||||
BeforeNewCommit BisectProgress = iota
|
||||
InbetweenCommits
|
||||
AfterOldCommit
|
||||
)
|
||||
|
||||
func GetCommitListDisplayStrings(
|
||||
commits []*models.Commit,
|
||||
fullDescription bool,
|
||||
|
@ -31,6 +40,7 @@ func GetCommitListDisplayStrings(
|
|||
startIdx int,
|
||||
length int,
|
||||
showGraph bool,
|
||||
bisectInfo *git_commands.BisectInfo,
|
||||
) [][]string {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
@ -77,12 +87,94 @@ func GetCommitListDisplayStrings(
|
|||
}
|
||||
|
||||
lines := make([][]string, 0, len(filteredCommits))
|
||||
bisectProgress := BeforeNewCommit
|
||||
var bisectStatus BisectStatus
|
||||
for i, commit := range filteredCommits {
|
||||
lines = append(lines, displayCommit(commit, cherryPickedCommitShaMap, diffName, parseEmoji, getGraphLine(i), fullDescription))
|
||||
bisectStatus, bisectProgress = getBisectStatus(commit.Sha, bisectInfo, bisectProgress)
|
||||
lines = append(lines, displayCommit(
|
||||
commit,
|
||||
cherryPickedCommitShaMap,
|
||||
diffName,
|
||||
parseEmoji,
|
||||
getGraphLine(i),
|
||||
fullDescription,
|
||||
bisectStatus,
|
||||
bisectInfo,
|
||||
))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// similar to the git_commands.BisectStatus but more gui-focused
|
||||
type BisectStatus int
|
||||
|
||||
const (
|
||||
BisectStatusNone BisectStatus = iota
|
||||
BisectStatusOld
|
||||
BisectStatusNew
|
||||
BisectStatusSkipped
|
||||
// adding candidate here which isn't present in the commands package because
|
||||
// we need to actually go through the commits to get this info
|
||||
BisectStatusCandidate
|
||||
// also adding this
|
||||
BisectStatusCurrent
|
||||
)
|
||||
|
||||
func getBisectStatus(commitSha string, bisectInfo *git_commands.BisectInfo, bisectProgress BisectProgress) (BisectStatus, BisectProgress) {
|
||||
if !bisectInfo.Started() {
|
||||
return BisectStatusNone, bisectProgress
|
||||
}
|
||||
|
||||
if bisectInfo.GetCurrentSha() == commitSha {
|
||||
return BisectStatusCurrent, bisectProgress
|
||||
}
|
||||
|
||||
status, ok := bisectInfo.Status(commitSha)
|
||||
if ok {
|
||||
switch status {
|
||||
case git_commands.BisectStatusNew:
|
||||
return BisectStatusNew, InbetweenCommits
|
||||
case git_commands.BisectStatusOld:
|
||||
return BisectStatusOld, AfterOldCommit
|
||||
case git_commands.BisectStatusSkipped:
|
||||
return BisectStatusSkipped, bisectProgress
|
||||
}
|
||||
} else {
|
||||
if bisectProgress == InbetweenCommits {
|
||||
return BisectStatusCandidate, bisectProgress
|
||||
} else {
|
||||
return BisectStatusNone, bisectProgress
|
||||
}
|
||||
}
|
||||
|
||||
// should never land here
|
||||
return BisectStatusNone, bisectProgress
|
||||
}
|
||||
|
||||
func getBisectStatusText(bisectStatus BisectStatus, bisectInfo *git_commands.BisectInfo) string {
|
||||
if bisectStatus == BisectStatusNone {
|
||||
return ""
|
||||
}
|
||||
|
||||
style := getBisectStatusColor(bisectStatus)
|
||||
|
||||
switch bisectStatus {
|
||||
case BisectStatusNew:
|
||||
return style.Sprintf("<-- " + bisectInfo.NewTerm())
|
||||
case BisectStatusOld:
|
||||
return style.Sprintf("<-- " + bisectInfo.OldTerm())
|
||||
case BisectStatusCurrent:
|
||||
// TODO: i18n
|
||||
return style.Sprintf("<-- current")
|
||||
case BisectStatusSkipped:
|
||||
return style.Sprintf("<-- skipped")
|
||||
case BisectStatusCandidate:
|
||||
return style.Sprintf("?")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func displayCommit(
|
||||
commit *models.Commit,
|
||||
cherryPickedCommitShaMap map[string]bool,
|
||||
|
@ -90,9 +182,11 @@ func displayCommit(
|
|||
parseEmoji bool,
|
||||
graphLine string,
|
||||
fullDescription bool,
|
||||
bisectStatus BisectStatus,
|
||||
bisectInfo *git_commands.BisectInfo,
|
||||
) []string {
|
||||
|
||||
shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap)
|
||||
shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap, bisectStatus, bisectInfo)
|
||||
bisectString := getBisectStatusText(bisectStatus, bisectInfo)
|
||||
|
||||
actionString := ""
|
||||
if commit.Action != "" {
|
||||
|
@ -122,6 +216,7 @@ func displayCommit(
|
|||
|
||||
cols := make([]string, 0, 5)
|
||||
cols = append(cols, shaColor.Sprint(commit.ShortSha()))
|
||||
cols = append(cols, bisectString)
|
||||
if fullDescription {
|
||||
cols = append(cols, style.FgBlue.Sprint(utils.UnixToDate(commit.UnixTimestamp)))
|
||||
}
|
||||
|
@ -133,10 +228,39 @@ func displayCommit(
|
|||
)
|
||||
|
||||
return cols
|
||||
|
||||
}
|
||||
|
||||
func getShaColor(commit *models.Commit, diffName string, cherryPickedCommitShaMap map[string]bool) style.TextStyle {
|
||||
func getBisectStatusColor(status BisectStatus) style.TextStyle {
|
||||
switch status {
|
||||
case BisectStatusNone:
|
||||
return style.FgBlack
|
||||
case BisectStatusNew:
|
||||
return style.FgRed
|
||||
case BisectStatusOld:
|
||||
return style.FgGreen
|
||||
case BisectStatusSkipped:
|
||||
return style.FgYellow
|
||||
case BisectStatusCurrent:
|
||||
return style.FgMagenta
|
||||
case BisectStatusCandidate:
|
||||
return style.FgBlue
|
||||
}
|
||||
|
||||
// shouldn't land here
|
||||
return style.FgWhite
|
||||
}
|
||||
|
||||
func getShaColor(
|
||||
commit *models.Commit,
|
||||
diffName string,
|
||||
cherryPickedCommitShaMap map[string]bool,
|
||||
bisectStatus BisectStatus,
|
||||
bisectInfo *git_commands.BisectInfo,
|
||||
) style.TextStyle {
|
||||
if bisectInfo.Started() {
|
||||
return getBisectStatusColor(bisectStatus)
|
||||
}
|
||||
|
||||
diffed := commit.Sha == diffName
|
||||
shaColor := theme.DefaultTextColor
|
||||
switch commit.Status {
|
||||
|
|
|
@ -28,6 +28,8 @@ const (
|
|||
REMOTES
|
||||
STATUS
|
||||
SUBMODULES
|
||||
// not actually a view. Will refactor this later
|
||||
BISECT_INFO
|
||||
)
|
||||
|
||||
func getScopeNames(scopes []RefreshableView) []string {
|
||||
|
@ -105,12 +107,12 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
|
|||
f := func() {
|
||||
var scopeMap map[RefreshableView]bool
|
||||
if len(options.scope) == 0 {
|
||||
scopeMap = arrToMap([]RefreshableView{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS})
|
||||
scopeMap = arrToMap([]RefreshableView{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS, BISECT_INFO})
|
||||
} else {
|
||||
scopeMap = arrToMap(options.scope)
|
||||
}
|
||||
|
||||
if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] {
|
||||
if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] || scopeMap[BISECT_INFO] {
|
||||
wg.Add(1)
|
||||
func() {
|
||||
if options.mode == ASYNC {
|
||||
|
|
|
@ -453,7 +453,24 @@ type TranslationSet struct {
|
|||
SortCommits string
|
||||
CantChangeContextSizeError string
|
||||
LcOpenCommitInBrowser string
|
||||
LcViewBisectOptions string
|
||||
Actions Actions
|
||||
Bisect Bisect
|
||||
}
|
||||
|
||||
type Bisect struct {
|
||||
MarkStart string
|
||||
MarkSkipCurrent string
|
||||
MarkSkipSelected string
|
||||
ResetTitle string
|
||||
ResetPrompt string
|
||||
ResetOption string
|
||||
BisectMenuTitle string
|
||||
Mark string
|
||||
Skip string
|
||||
CompleteTitle string
|
||||
CompletePrompt string
|
||||
CompletePromptIndeterminate string
|
||||
}
|
||||
|
||||
type Actions struct {
|
||||
|
@ -541,6 +558,10 @@ type Actions struct {
|
|||
OpenMergeTool string
|
||||
OpenCommitInBrowser string
|
||||
OpenPullRequest string
|
||||
StartBisect string
|
||||
ResetBisect string
|
||||
BisectSkip string
|
||||
BisectMark string
|
||||
}
|
||||
|
||||
const englishIntroPopupMessage = `
|
||||
|
@ -1005,6 +1026,7 @@ func EnglishTranslationSet() TranslationSet {
|
|||
SortCommits: "commit sort order",
|
||||
CantChangeContextSizeError: "Cannot change context while in patch building mode because we were too lazy to support it when releasing the feature. If you really want it, please let us know!",
|
||||
LcOpenCommitInBrowser: "open commit in browser",
|
||||
LcViewBisectOptions: "view bisect options",
|
||||
Actions: Actions{
|
||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||
CheckoutCommit: "Checkout commit",
|
||||
|
@ -1091,6 +1113,22 @@ func EnglishTranslationSet() TranslationSet {
|
|||
OpenMergeTool: "Open merge tool",
|
||||
OpenCommitInBrowser: "Open commit in browser",
|
||||
OpenPullRequest: "Open pull request in browser",
|
||||
StartBisect: "Start bisect",
|
||||
ResetBisect: "Reset bisect",
|
||||
BisectSkip: "Bisect skip",
|
||||
BisectMark: "Bisect mark",
|
||||
},
|
||||
Bisect: Bisect{
|
||||
Mark: "mark %s as %s",
|
||||
MarkStart: "mark %s as %s (start bisect)",
|
||||
Skip: "skip %s",
|
||||
ResetTitle: "Reset 'git bisect'",
|
||||
ResetPrompt: "Are you sure you want to reset 'git bisect'?",
|
||||
ResetOption: "reset bisect",
|
||||
BisectMenuTitle: "Bisect",
|
||||
CompleteTitle: "Bisect complete",
|
||||
CompletePrompt: "Bisect complete! The following commit introduced the change:\n\n%s\n\nDo you want to reset 'git bisect' now?",
|
||||
CompletePromptIndeterminate: "Bisect complete! Some commits were skipped, so any of the following commits may have introduced the change:\n\n%s\n\nDo you want to reset 'git bisect' now?",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,3 +121,10 @@ func SafeTruncate(str string, limit int) string {
|
|||
return str
|
||||
}
|
||||
}
|
||||
|
||||
func ShortSha(sha string) string {
|
||||
if len(sha) < 8 {
|
||||
return sha
|
||||
}
|
||||
return sha[:8]
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
dd9d90ed2d1fa5a284adba081199f18458977547
|
16
test/integration/bisect/expected/.git_keep/BISECT_LOG
Normal file
16
test/integration/bisect/expected/.git_keep/BISECT_LOG
Normal file
|
@ -0,0 +1,16 @@
|
|||
git bisect start
|
||||
# bad: [1e2780095cd8e95b93f89268f72cda21d528ab38] commit 19
|
||||
git bisect bad 1e2780095cd8e95b93f89268f72cda21d528ab38
|
||||
# good: [fbbb6006074afe8cc9009b649fae19f920b604ea] commit 10
|
||||
git bisect good fbbb6006074afe8cc9009b649fae19f920b604ea
|
||||
# skip: [1b06712fea4c03c8fce8e2b3862c059f8d7f8268] commit 11
|
||||
git bisect skip 1b06712fea4c03c8fce8e2b3862c059f8d7f8268
|
||||
# skip: [ee930b55b61910c0830b0c6ea1cf9ada066d27fc] commit 12
|
||||
git bisect skip ee930b55b61910c0830b0c6ea1cf9ada066d27fc
|
||||
# bad: [688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed] commit 15
|
||||
git bisect bad 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed
|
||||
# good: [ac324deab67f1749eeec1531b37dfaff41e559ca] commit 13
|
||||
git bisect good ac324deab67f1749eeec1531b37dfaff41e559ca
|
||||
# bad: [dd9d90ed2d1fa5a284adba081199f18458977547] commit 14
|
||||
git bisect bad dd9d90ed2d1fa5a284adba081199f18458977547
|
||||
# first bad commit: [dd9d90ed2d1fa5a284adba081199f18458977547] commit 14
|
1
test/integration/bisect/expected/.git_keep/BISECT_NAMES
Normal file
1
test/integration/bisect/expected/.git_keep/BISECT_NAMES
Normal file
|
@ -0,0 +1 @@
|
|||
|
1
test/integration/bisect/expected/.git_keep/BISECT_START
Normal file
1
test/integration/bisect/expected/.git_keep/BISECT_START
Normal file
|
@ -0,0 +1 @@
|
|||
master
|
2
test/integration/bisect/expected/.git_keep/BISECT_TERMS
Normal file
2
test/integration/bisect/expected/.git_keep/BISECT_TERMS
Normal file
|
@ -0,0 +1,2 @@
|
|||
bad
|
||||
good
|
|
@ -0,0 +1 @@
|
|||
commit 20
|
0
test/integration/bisect/expected/.git_keep/FETCH_HEAD
Normal file
0
test/integration/bisect/expected/.git_keep/FETCH_HEAD
Normal file
1
test/integration/bisect/expected/.git_keep/HEAD
Normal file
1
test/integration/bisect/expected/.git_keep/HEAD
Normal file
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/test
|
10
test/integration/bisect/expected/.git_keep/config
Normal file
10
test/integration/bisect/expected/.git_keep/config
Normal file
|
@ -0,0 +1,10 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
ignorecase = true
|
||||
precomposeunicode = true
|
||||
[user]
|
||||
email = CI@example.com
|
||||
name = CI
|
1
test/integration/bisect/expected/.git_keep/description
Normal file
1
test/integration/bisect/expected/.git_keep/description
Normal file
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
BIN
test/integration/bisect/expected/.git_keep/index
Normal file
BIN
test/integration/bisect/expected/.git_keep/index
Normal file
Binary file not shown.
7
test/integration/bisect/expected/.git_keep/info/exclude
Normal file
7
test/integration/bisect/expected/.git_keep/info/exclude
Normal file
|
@ -0,0 +1,7 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
.DS_Store
|
25
test/integration/bisect/expected/.git_keep/logs/HEAD
Normal file
25
test/integration/bisect/expected/.git_keep/logs/HEAD
Normal file
|
@ -0,0 +1,25 @@
|
|||
0000000000000000000000000000000000000000 b7cd988a171f7f99b7e190ca2b46060074cb379a CI <CI@example.com> 1642807341 +1100 commit (initial): commit 1
|
||||
b7cd988a171f7f99b7e190ca2b46060074cb379a 810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 CI <CI@example.com> 1642807341 +1100 commit: commit 2
|
||||
810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 98c3099431a8777741ea114272919d6645418037 CI <CI@example.com> 1642807341 +1100 commit: commit 3
|
||||
98c3099431a8777741ea114272919d6645418037 f8174a4db5bb2082ebe73b29a47448a300fea7ae CI <CI@example.com> 1642807342 +1100 commit: commit 4
|
||||
f8174a4db5bb2082ebe73b29a47448a300fea7ae 305a009f27eb14858ea0a3a1a740a5346a543537 CI <CI@example.com> 1642807342 +1100 commit: commit 5
|
||||
305a009f27eb14858ea0a3a1a740a5346a543537 5530322be194fc9dea08ef86c9306bddeacb92db CI <CI@example.com> 1642807342 +1100 commit: commit 6
|
||||
5530322be194fc9dea08ef86c9306bddeacb92db 2cdabd5c24e74e22323744543a8ebcbfb33c7f6e CI <CI@example.com> 1642807342 +1100 commit: commit 7
|
||||
2cdabd5c24e74e22323744543a8ebcbfb33c7f6e 42fb40334713a02429d4f8d72f7fe7376caef15b CI <CI@example.com> 1642807342 +1100 commit: commit 8
|
||||
42fb40334713a02429d4f8d72f7fe7376caef15b 27584027b768a0d33ba92ad8784c09589de325b9 CI <CI@example.com> 1642807342 +1100 commit: commit 9
|
||||
27584027b768a0d33ba92ad8784c09589de325b9 fbbb6006074afe8cc9009b649fae19f920b604ea CI <CI@example.com> 1642807342 +1100 commit: commit 10
|
||||
fbbb6006074afe8cc9009b649fae19f920b604ea 1b06712fea4c03c8fce8e2b3862c059f8d7f8268 CI <CI@example.com> 1642807342 +1100 commit: commit 11
|
||||
1b06712fea4c03c8fce8e2b3862c059f8d7f8268 ee930b55b61910c0830b0c6ea1cf9ada066d27fc CI <CI@example.com> 1642807342 +1100 commit: commit 12
|
||||
ee930b55b61910c0830b0c6ea1cf9ada066d27fc ac324deab67f1749eeec1531b37dfaff41e559ca CI <CI@example.com> 1642807342 +1100 commit: commit 13
|
||||
ac324deab67f1749eeec1531b37dfaff41e559ca dd9d90ed2d1fa5a284adba081199f18458977547 CI <CI@example.com> 1642807342 +1100 commit: commit 14
|
||||
dd9d90ed2d1fa5a284adba081199f18458977547 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed CI <CI@example.com> 1642807342 +1100 commit: commit 15
|
||||
688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed 2ce3bf88f382c762d97ac069eea18bed43a1bab2 CI <CI@example.com> 1642807342 +1100 commit: commit 16
|
||||
2ce3bf88f382c762d97ac069eea18bed43a1bab2 c09b924073b6a6cc1b2208f9a00f7b73bec2add2 CI <CI@example.com> 1642807342 +1100 commit: commit 17
|
||||
c09b924073b6a6cc1b2208f9a00f7b73bec2add2 12a951328c3156482355edebf6c81ded5480aff4 CI <CI@example.com> 1642807342 +1100 commit: commit 18
|
||||
12a951328c3156482355edebf6c81ded5480aff4 1e2780095cd8e95b93f89268f72cda21d528ab38 CI <CI@example.com> 1642807342 +1100 commit: commit 19
|
||||
1e2780095cd8e95b93f89268f72cda21d528ab38 1fd41e04d86ee95083d607da4e22abef9a570abc CI <CI@example.com> 1642807342 +1100 commit: commit 20
|
||||
1fd41e04d86ee95083d607da4e22abef9a570abc dd9d90ed2d1fa5a284adba081199f18458977547 CI <CI@example.com> 1642807346 +1100 checkout: moving from master to dd9d90ed2d1fa5a284adba081199f18458977547
|
||||
dd9d90ed2d1fa5a284adba081199f18458977547 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed CI <CI@example.com> 1642807348 +1100 checkout: moving from dd9d90ed2d1fa5a284adba081199f18458977547 to 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed
|
||||
688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed ac324deab67f1749eeec1531b37dfaff41e559ca CI <CI@example.com> 1642807352 +1100 checkout: moving from 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed to ac324deab67f1749eeec1531b37dfaff41e559ca
|
||||
ac324deab67f1749eeec1531b37dfaff41e559ca dd9d90ed2d1fa5a284adba081199f18458977547 CI <CI@example.com> 1642807354 +1100 checkout: moving from ac324deab67f1749eeec1531b37dfaff41e559ca to dd9d90ed2d1fa5a284adba081199f18458977547
|
||||
dd9d90ed2d1fa5a284adba081199f18458977547 1fd41e04d86ee95083d607da4e22abef9a570abc CI <CI@example.com> 1642807358 +1100 checkout: moving from dd9d90ed2d1fa5a284adba081199f18458977547 to test
|
|
@ -0,0 +1,20 @@
|
|||
0000000000000000000000000000000000000000 b7cd988a171f7f99b7e190ca2b46060074cb379a CI <CI@example.com> 1642807341 +1100 commit (initial): commit 1
|
||||
b7cd988a171f7f99b7e190ca2b46060074cb379a 810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 CI <CI@example.com> 1642807341 +1100 commit: commit 2
|
||||
810c10a66b1dabfe117eecdfb0f638bb1cd0ede5 98c3099431a8777741ea114272919d6645418037 CI <CI@example.com> 1642807341 +1100 commit: commit 3
|
||||
98c3099431a8777741ea114272919d6645418037 f8174a4db5bb2082ebe73b29a47448a300fea7ae CI <CI@example.com> 1642807342 +1100 commit: commit 4
|
||||
f8174a4db5bb2082ebe73b29a47448a300fea7ae 305a009f27eb14858ea0a3a1a740a5346a543537 CI <CI@example.com> 1642807342 +1100 commit: commit 5
|
||||
305a009f27eb14858ea0a3a1a740a5346a543537 5530322be194fc9dea08ef86c9306bddeacb92db CI <CI@example.com> 1642807342 +1100 commit: commit 6
|
||||
5530322be194fc9dea08ef86c9306bddeacb92db 2cdabd5c24e74e22323744543a8ebcbfb33c7f6e CI <CI@example.com> 1642807342 +1100 commit: commit 7
|
||||
2cdabd5c24e74e22323744543a8ebcbfb33c7f6e 42fb40334713a02429d4f8d72f7fe7376caef15b CI <CI@example.com> 1642807342 +1100 commit: commit 8
|
||||
42fb40334713a02429d4f8d72f7fe7376caef15b 27584027b768a0d33ba92ad8784c09589de325b9 CI <CI@example.com> 1642807342 +1100 commit: commit 9
|
||||
27584027b768a0d33ba92ad8784c09589de325b9 fbbb6006074afe8cc9009b649fae19f920b604ea CI <CI@example.com> 1642807342 +1100 commit: commit 10
|
||||
fbbb6006074afe8cc9009b649fae19f920b604ea 1b06712fea4c03c8fce8e2b3862c059f8d7f8268 CI <CI@example.com> 1642807342 +1100 commit: commit 11
|
||||
1b06712fea4c03c8fce8e2b3862c059f8d7f8268 ee930b55b61910c0830b0c6ea1cf9ada066d27fc CI <CI@example.com> 1642807342 +1100 commit: commit 12
|
||||
ee930b55b61910c0830b0c6ea1cf9ada066d27fc ac324deab67f1749eeec1531b37dfaff41e559ca CI <CI@example.com> 1642807342 +1100 commit: commit 13
|
||||
ac324deab67f1749eeec1531b37dfaff41e559ca dd9d90ed2d1fa5a284adba081199f18458977547 CI <CI@example.com> 1642807342 +1100 commit: commit 14
|
||||
dd9d90ed2d1fa5a284adba081199f18458977547 688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed CI <CI@example.com> 1642807342 +1100 commit: commit 15
|
||||
688bdfce6d5b16ebd7f5c6d6d5d68de7ea5344ed 2ce3bf88f382c762d97ac069eea18bed43a1bab2 CI <CI@example.com> 1642807342 +1100 commit: commit 16
|
||||
2ce3bf88f382c762d97ac069eea18bed43a1bab2 c09b924073b6a6cc1b2208f9a00f7b73bec2add2 CI <CI@example.com> 1642807342 +1100 commit: commit 17
|
||||
c09b924073b6a6cc1b2208f9a00f7b73bec2add2 12a951328c3156482355edebf6c81ded5480aff4 CI <CI@example.com> 1642807342 +1100 commit: commit 18
|
||||
12a951328c3156482355edebf6c81ded5480aff4 1e2780095cd8e95b93f89268f72cda21d528ab38 CI <CI@example.com> 1642807342 +1100 commit: commit 19
|
||||
1e2780095cd8e95b93f89268f72cda21d528ab38 1fd41e04d86ee95083d607da4e22abef9a570abc CI <CI@example.com> 1642807342 +1100 commit: commit 20
|
|
@ -0,0 +1 @@
|
|||
0000000000000000000000000000000000000000 1fd41e04d86ee95083d607da4e22abef9a570abc CI <CI@example.com> 1642807358 +1100 branch: Created from master
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
x<01>ŽK
|
||||
Â0@]ç³d2‰ù€ˆÐU<C390>1™LP°¶”ß,<€ÛÇãñd]–G›Ã¡ïªà]L=ÖÆX5rŽŽl(g_rf'èZrb6ÞõÕA0—L£+<2B>ƒˆ-D˜ZfÄË *ĵ’áw¿¯;L3\¦ù¦^¶§žd]®`ƒ§4
|
||||
žàh-¢tLuýSÿù`“ùEš:³
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
x<01><>Á
|
||||
1D=÷+z$I›n"žö3j›¢`Ýe©à盃 s›y3eíý1,¦p»ˆMèBsȉ ¼k(“”Z R,Q²ªNh¶¼ËK‹”£ÓPKÁGrÌRåÖB‰X¥²<C2A5><C2B2>[ó&¿Ç}Ýí¼Øó¼\å“ûö”SYûÅbðaržìÀ¨«£†ü‰ÿx=a¾5b:•
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
x<01>ŽK
|
||||
1D]çÙ’oO"¬æét7
|
||||
Ɔß,<€Ë*Þ+ª®=ºõg8ô]ÄFU$§ƒSB‘KŽ%º¨çT‹FÇ52[ÙåÕ- kàL„xÒ\<5C>Gd@–I†ž’°)ï~_w;/ö2/7ù”¶=åT×vµR@7ÅìÑ{çÌhÇ©.â?~ì˜/uo<«
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
x+)JMU06b040031QHֻּIe<49>כ6פLסQ»תא)<29>¥¹:‹ױ<E280B9><…נפ
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
x<01><>A
|
||||
Β0E]ηΩ23<32>™D„®z<C2AE>Ιd<CE99>‚µ¥Dπψfαά}ή{‹―λ²<<3C>Η|>΄έΜΧ¬!™Υz®Τχ ‰<>
|
||||
ΜQJλ¦ΊMv{5_XkNI<4E>qζ9ηΒ†T¨„€ƒ–<C692>³8y·ϋΊϋqς—qΊΩG–νi']—«Η(ύΐuΪO5ϋ3<CF8B>υ<EFBFBD>άΌ<>;P
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
x+)JMU06b040031QHклIeь▒■ЦС_ц3и[}ШY╝Кзbf┘k░
|
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
x█мA
|
||||
ц @я╝=еЛеQ'PB ╚cт▒*√`║гO=@╥÷?Вж·0дкьU║╨≤Iт▒■д~╒Б≥p┼■дW▌RX╙ьdД3}┤u┐Ш╨-З∙Ж~И-В6RplёWDkмYоип?Ыо u∙-#
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue