1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-09 13:47:11 +02:00

pull commit list builder functions into their own builder struct

This commit is contained in:
Jesse Duffield 2019-02-19 09:18:30 +11:00
parent a8e22ed82f
commit dcc7855fd0
4 changed files with 284 additions and 240 deletions

View File

@ -2,16 +2,12 @@ package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/mgutz/str"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
@ -250,21 +246,6 @@ func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
// GetUnpushedCommits Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *GitCommand) GetUnpushedCommits() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
@ -527,226 +508,6 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
}
func (c *GitCommand) getMergeBase() (string, error) {
currentBranch, err := c.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
if err != nil {
// swallowing error because it's not a big deal; probably because there are no commits yet
}
return output, nil
}
// GetRebasingCommits obtains the commits that we're in the process of rebasing
func (c *GitCommand) GetRebasingCommits() ([]*Commit, error) {
rebaseMode, err := c.RebaseMode()
if err != nil {
return nil, err
}
switch rebaseMode {
case "normal":
return c.GetNormalRebasingCommits()
case "interactive":
return c.GetInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *GitCommand) GetNormalRebasingCommits() ([]*Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(".git/rebase-apply/rewritten")
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*Commit{}
err = filepath.Walk(".git/rebase-apply", func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount -= 1
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.CommitFromPatch(content)
if err != nil {
return err
}
commits = append([]*Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// GetInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *GitCommand) GetInteractiveRebasingCommits() ([]*Commit, error) {
bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo")
var content []string
if err == nil {
content = strings.Split(string(bytesContent), "\n")
if len(content) > 0 && content[len(content)-1] == "" {
content = content[0 : len(content)-1]
}
}
// for each of them, grab the matching commit name in the backup
bytesContent, err = ioutil.ReadFile(".git/rebase-merge/git-rebase-todo.backup")
var backupContent []string
if err == nil {
backupContent = strings.Split(string(bytesContent), "\n")
}
commits := []*Commit{}
for _, todoLine := range content {
commit := c.extractCommit(todoLine, backupContent)
if commit != nil {
commits = append([]*Commit{commit}, commits...)
}
}
return commits, nil
}
func (c *GitCommand) extractCommit(todoLine string, backupContent []string) *Commit {
for _, backupLine := range backupContent {
split := strings.Split(todoLine, " ")
prefix := strings.Join(split[0:2], " ")
if strings.HasPrefix(backupLine, prefix) {
return &Commit{
Sha: split[2],
Name: strings.TrimPrefix(backupLine, prefix+" "),
Status: "rebasing",
}
}
}
return nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *GitCommand) CommitFromPatch(content string) (*Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1][0:7]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
// GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() ([]*Commit, error) {
commits := []*Commit{}
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err := c.GetRebasingCommits()
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
unpushedCommits := c.GetUnpushedCommits()
log := c.GetLog()
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Status: status,
DisplayString: strings.Join(splitLine, " "),
})
}
if len(rebasingCommits) > 0 {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprint("<-- YOU ARE HERE ---")
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
return c.setCommitMergedStatuses(commits)
}
func (c *GitCommand) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
ancestor, err := c.getMergeBase()
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
// GetLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *GitCommand) GetLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)

View File

@ -20,6 +20,9 @@ import (
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry

View File

@ -0,0 +1,275 @@
package git
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand) (*CommitListBuilder, error) {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
}, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
commits := []*commands.Commit{}
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err := c.getRebasingCommits()
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog()
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &commands.Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Status: status,
DisplayString: strings.Join(splitLine, " "),
})
}
if len(rebasingCommits) > 0 {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprint("<-- YOU ARE HERE ---")
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
return c.setCommitMergedStatuses(commits)
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits() ([]*commands.Commit, error) {
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
switch rebaseMode {
case "normal":
return c.getNormalRebasingCommits()
case "interactive":
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(".git/rebase-apply/rewritten")
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*commands.Commit{}
err = filepath.Walk(".git/rebase-apply", func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commits = append([]*commands.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) {
bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo")
var content []string
if err == nil {
content = strings.Split(string(bytesContent), "\n")
if len(content) > 0 && content[len(content)-1] == "" {
content = content[0 : len(content)-1]
}
}
// for each of them, grab the matching commit name in the backup
bytesContent, err = ioutil.ReadFile(".git/rebase-merge/git-rebase-todo.backup")
var backupContent []string
if err == nil {
backupContent = strings.Split(string(bytesContent), "\n")
}
commits := []*commands.Commit{}
for _, todoLine := range content {
commit := c.extractCommit(todoLine, backupContent)
if commit != nil {
commits = append([]*commands.Commit{commit}, commits...)
}
}
return commits, nil
}
func (c *CommitListBuilder) extractCommit(todoLine string, backupContent []string) *commands.Commit {
for _, backupLine := range backupContent {
split := strings.Split(todoLine, " ")
prefix := strings.Join(split[0:2], " ")
if strings.HasPrefix(backupLine, prefix) {
return &commands.Commit{
Sha: split[2],
Name: strings.TrimPrefix(backupLine, prefix+" "),
Status: "rebasing",
}
}
}
return nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1][0:7]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &commands.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
ancestor, err := c.getMergeBase()
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase() (string, error) {
currentBranch, err := c.GitCommand.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
if err != nil {
// swallowing error because it's not a big deal; probably because there are no commits yet
}
return output, nil
}
// getUnpushedCommits Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// getLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *CommitListBuilder) getLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}

View File

@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@ -39,7 +40,11 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
commits, err := gui.GitCommand.GetCommits()
builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand)
if err != nil {
return err
}
commits, err := builder.GetCommits()
if err != nil {
return err
}