add support for git bisect

This commit is contained in:
Jesse Duffield 2022-01-19 18:32:27 +11:00
parent ab84410b41
commit 4ab5e54139
117 changed files with 1013 additions and 104 deletions

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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),

View 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
}

View 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
}

View file

@ -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

View file

@ -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 {

View file

@ -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)
}

View file

@ -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())
}

View file

@ -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
View 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{}})
}

View file

@ -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,
},
)

View file

@ -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)
}

View file

@ -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)
})
}

View file

@ -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)
})
}

View 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
}
}

View file

@ -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}},

View file

@ -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)},

View file

@ -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) {

View file

@ -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),
)
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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?",
},
}
}

View file

@ -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]
}

View file

@ -0,0 +1 @@
dd9d90ed2d1fa5a284adba081199f18458977547

View 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

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@
master

View file

@ -0,0 +1,2 @@
bad
good

View file

@ -0,0 +1 @@
commit 20

View file

@ -0,0 +1 @@
ref: refs/heads/test

View 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

View file

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

Binary file not shown.

View 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

View 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

View file

@ -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

View file

@ -0,0 +1 @@
0000000000000000000000000000000000000000 1fd41e04d86ee95083d607da4e22abef9a570abc CI <CI@example.com> 1642807358 +1100 branch: Created from master

View file

@ -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š:³

View file

@ -0,0 +1,2 @@
x<01><>Á
1 D=÷+z$In "žö3j¢`Ýe©àçƒ sy3eíý1,¦p»ˆMèBsȉ ¼k(“”Z R,Q²ªNh¶¼ËK£ÓPKÁGrÌRåÖB‰X¥²<C2A5><C2B2>[ó&¿Ç}Ýí¼Øó¼\å“ûö”SYûÅbðaržìÀ¨«£†ü‰ÿx=a¾5b:•

View file

@ -0,0 +1,3 @@
x<01>ŽK
1D]çÙ oO "¬æét7
Æß,<€Ë*Þ+ª®­=ºõg8ô]ÄFU$§ƒSBKŽ%º¨çTFÇ52[ÙåÕ- kàL„xÒ\<5C>Gd@I†ž°)ï~_w;/ö2/7ù”¶=åT×R@7ÅìÑ{çÌhÇ©.â?~ì˜/uo<«

View file

@ -0,0 +1 @@
x+)JMU06b040031QHֻּIe<49>כ6פLסQ»תא)<29>¥¹:ױ<E280B9><…נפ

View file

@ -0,0 +1,3 @@
x<01><>A
Β0E]ηΩ 23<32>D„®z<C2AE>Ιd<CE99>µ¥Dπψfαά}ή{‹―λ²<<3C>Η|>΄έΜΧ¬!™ΥΤχ <>
ΜQJλ¦ΊMv{5_XkNI<4E>qζ9ηΒT¨„€ƒ<C692>³8y·ϋΊϋqς—qΊΩGνi']—«Η(ύΐuΪO5ϋ3<CF8B>υ<EFBFBD>άΌ<>;P

View file

@ -0,0 +1 @@
x+)JMU06b040031QHклIeь▒■ЦС_ц3и[}ШY╝Кзbf┘k

View file

@ -0,0 +1,2 @@
x█мA
ц @я╝=еЛ еQ'PB ╚cт▒*√`║гO=@╥÷?Вж·0дкьU║╨≤Iт▒■д~╒Б≥p┼■дW▌RX╙ьdД3}┤u┐Ш╨-З∙Ж~И-В6RplёWDkмYоип?Ыо u∙-#

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