Skip to content

Commit

Permalink
fix: Refactor git execution, add tests
Browse files Browse the repository at this point in the history
Signed-off-by: Valentin Kiselev <mrexox@evilmartians.com>
  • Loading branch information
mrexox committed Feb 7, 2023
1 parent b688631 commit 1d211d5
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 96 deletions.
64 changes: 64 additions & 0 deletions internal/git/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package git

import (
"os"
"os/exec"
"strings"
)

type Exec interface {
Cmd(cmd string) (string, error)
CmdArgs(args ...string) (string, error)
CmdLines(cmd string) ([]string, error)
RawCmd(cmd string) (string, error)
}

type OsExec struct{}

// NewOsExec returns an object that executes given commands
// in the OS.
func NewOsExec() *OsExec {
return &OsExec{}
}

func (o *OsExec) Cmd(cmd string) (string, error) {
args := strings.Split(cmd, " ")
return o.CmdArgs(args...)
}

func (o *OsExec) CmdLines(cmd string) ([]string, error) {
out, err := o.RawCmd(cmd)
if err != nil {
return nil, err
}

return strings.Split(out, "\n"), nil
}

func (o *OsExec) CmdArgs(args ...string) (string, error) {
out, err := o.rawExecArgs(args...)
if err != nil {
return "", err
}

return strings.TrimSpace(out), nil
}

func (o *OsExec) RawCmd(cmd string) (string, error) {
args := strings.Split(cmd, " ")
return o.rawExecArgs(args...)
}

// rawExecArgs executes git command with LEFTHOOK=0 in order
// to prevent calling subsequent lefthook hooks.
func (o *OsExec) rawExecArgs(args ...string) (string, error) {
cmd := exec.Command(args[0], args[1:]...)
cmd.Env = append(os.Environ(), "LEFTHOOK=0")

out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}

return string(out), nil
}
16 changes: 9 additions & 7 deletions internal/git/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,22 @@ func (r *Repository) updateRemote(path, ref string) error {
log.Debugf("Updating remote config repository: %s", path)

if len(ref) != 0 {
cmdFetch := []string{"git", "-C", path, "fetch", "--quiet", "--depth", "1", "origin", ref}
_, err := execGit(strings.Join(cmdFetch, " "))
_, err := r.Git.CmdArgs(
"git", "-C", path, "fetch", "--quiet", "--depth", "1",
"origin", ref,
)
if err != nil {
return err
}

cmdFetch = []string{"git", "-C", path, "checkout", "FETCH_HEAD"}
_, err = execGit(strings.Join(cmdFetch, " "))
_, err = r.Git.CmdArgs(
"git", "-C", path, "checkout", "FETCH_HEAD",
)
if err != nil {
return err
}
} else {
cmdFetch := []string{"git", "-C", path, "pull", "--quiet"}
_, err := execGit(strings.Join(cmdFetch, " "))
_, err := r.Git.CmdArgs("git", "-C", path, "pull", "--quiet")
if err != nil {
return err
}
Expand All @@ -99,7 +101,7 @@ func (r *Repository) cloneRemote(path, url, ref string) error {
}
cmdClone = append(cmdClone, url)

_, err := execGit(strings.Join(cmdClone, " "))
_, err := r.Git.CmdArgs(cmdClone...)
if err != nil {
return err
}
Expand Down
149 changes: 72 additions & 77 deletions internal/git/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package git

import (
"os"
"os/exec"
"path/filepath"
"runtime"
"regexp"
"strings"

"github.com/spf13/afero"
Expand All @@ -18,15 +17,20 @@ const (
cmdStagedFiles = "git diff --name-only --cached --diff-filter=ACMR"
cmdAllFiles = "git ls-files --cached"
cmdPushFiles = "git diff --name-only HEAD @{push} || git diff --name-only HEAD master"
infoDirMode = 0o775
cmdStatusShort = "git status --short"
cmdCreateStash = "git stash create"
stashMessage = "lefthook auto backup"
cmdListStash = "git stash list"

stashMessage = "lefthook auto backup"
unstagedPatchName = "lefthook-unstaged.patch"
infoDirMode = 0o775
minStatusLen = 3
)

// Repository represents a git repository.
type Repository struct {
Fs afero.Fs
Git Exec
HooksPath string
RootPath string
GitPath string
Expand All @@ -35,21 +39,21 @@ type Repository struct {
}

// NewRepository returns a Repository or an error, if git repository it not initialized.
func NewRepository(fs afero.Fs) (*Repository, error) {
rootPath, err := execGit(cmdRootPath)
func NewRepository(fs afero.Fs, git Exec) (*Repository, error) {
rootPath, err := git.Cmd(cmdRootPath)
if err != nil {
return nil, err
}

hooksPath, err := execGit(cmdHooksPath)
hooksPath, err := git.Cmd(cmdHooksPath)
if err != nil {
return nil, err
}
if exists, _ := afero.DirExists(fs, filepath.Join(rootPath, hooksPath)); exists {
hooksPath = filepath.Join(rootPath, hooksPath)
}

infoPath, err := execGit(cmdInfoPath)
infoPath, err := git.Cmd(cmdInfoPath)
if err != nil {
return nil, err
}
Expand All @@ -61,7 +65,7 @@ func NewRepository(fs afero.Fs) (*Repository, error) {
}
}

gitPath, err := execGit(cmdGitPath)
gitPath, err := git.Cmd(cmdGitPath)
if err != nil {
return nil, err
}
Expand All @@ -71,11 +75,12 @@ func NewRepository(fs afero.Fs) (*Repository, error) {

return &Repository{
Fs: fs,
Git: git,
HooksPath: hooksPath,
RootPath: rootPath,
GitPath: gitPath,
InfoPath: infoPath,
unstagedPatchPath: filepath.Join(infoPath, "lefthook-unstaged.patch"),
unstagedPatchPath: filepath.Join(infoPath, unstagedPatchName),
}, nil
}

Expand All @@ -101,21 +106,28 @@ func (r *Repository) PushFiles() ([]string, error) {
// unstaged changes.
// See https://git-scm.com/docs/git-status#_short_format.
func (r *Repository) PartiallyStagedFiles() ([]string, error) {
lines, err := r.gitLines(cmdStatusShort)
lines, err := r.Git.CmdLines(cmdStatusShort)
if err != nil {
return []string{}, err
}

partiallyStaged := make([]string, 0)

for _, line := range lines {
if len(line) < 3 {
if len(line) < minStatusLen {
continue
}
m1 := line[0] // index
m2 := line[1] // working tree

index := line[0]
workingTree := line[1]

filename := line[3:]
if m1 != ' ' && m1 != '?' && m2 != ' ' && m2 != '?' && len(filename) > 0 {
idx := strings.Index(filename, "->")
if idx != -1 {
filename = filename[idx+3:]
}

if index != ' ' && index != '?' && workingTree != ' ' && workingTree != '?' && len(filename) > 0 {
partiallyStaged = append(partiallyStaged, filename)
}
}
Expand All @@ -124,16 +136,16 @@ func (r *Repository) PartiallyStagedFiles() ([]string, error) {
}

func (r *Repository) SaveUnstaged(files []string) error {
_, err := execGitCmd(
_, err := r.Git.CmdArgs(
append([]string{
"git",
"diff",
"--binary", // support binary files
"--unified=0", // do not add lines around diff for consistent behaviour
"--no-color", // disable colors for consistent behaviour
"--no-ext-diff", // disable external diff tools for consistent behaviour
"--src-prefix=a/", // force prefix for consistent behaviour
"--dst-prefix=b/", // force prefix for consistent behaviour
"--unified=0", // do not add lines around diff for consistent behavior
"--no-color", // disable colors for consistent behavior
"--no-ext-diff", // disable external diff tools for consistent behavior
"--src-prefix=a/", // force prefix for consistent behavior
"--dst-prefix=b/", // force prefix for consistent behavior
"--patch", // output a patch that can be applied
"--submodule=short", // always use the default short format for submodules
"--output",
Expand All @@ -146,7 +158,7 @@ func (r *Repository) SaveUnstaged(files []string) error {
}

func (r *Repository) HideUnstaged(files []string) error {
_, err := execGitCmd(
_, err := r.Git.CmdArgs(
append([]string{
"git",
"checkout",
Expand All @@ -163,7 +175,7 @@ func (r *Repository) RestoreUnstaged() error {
return nil
}

_, err := execGitCmd(
_, err := r.Git.CmdArgs(
"git",
"apply",
"-v",
Expand All @@ -180,13 +192,13 @@ func (r *Repository) RestoreUnstaged() error {
return err
}

func (r *Repository) StashUnstaged() (string, error) {
stashHash, err := execGit(cmdCreateStash)
func (r *Repository) StashUnstaged() error {
stashHash, err := r.Git.Cmd(cmdCreateStash)
if err != nil {
return "", err
return err
}

_, err = execGitCmd(
_, err = r.Git.CmdArgs(
"git",
"stash",
"store",
Expand All @@ -196,51 +208,53 @@ func (r *Repository) StashUnstaged() (string, error) {
stashHash,
)
if err != nil {
return "", err
return err
}

return stashHash, nil
}

func (r *Repository) DropUnstagedStash(hash string) error {
_, err := execGitCmd(
"git",
"stash",
"drop",
"--quiet",
hash,
)

return err
return nil
}

// FilesByCommand accepts git command and returns its result as a list of filepaths.
func (r *Repository) FilesByCommand(command string) ([]string, error) {

lines, err := r.gitLines(command)
func (r *Repository) DropUnstagedStash() error {
lines, err := r.Git.CmdLines(cmdListStash)
if err != nil {
return nil, err
return err
}

return r.extractFiles(lines)
}

func (r *Repository) gitLines(command string) ([]string, error) {
var cmd *exec.Cmd
stashRegexp := regexp.MustCompile(`^(?P<stash>[^ ]+):\s*` + stashMessage)
for i := range lines {
line := lines[len(lines)-i-1]
matches := stashRegexp.FindStringSubmatch(line)
if len(matches) == 0 {
continue
}

if runtime.GOOS == "windows" {
commandArg := strings.Split(command, " ")
cmd = exec.Command(commandArg[0], commandArg[1:]...)
} else {
cmd = exec.Command("sh", "-c", command)
stashID := stashRegexp.SubexpIndex("stash")

if len(matches[stashID]) > 0 {
_, err := r.Git.CmdArgs(
"git",
"stash",
"drop",
"--quiet",
matches[stashID],
)
if err != nil {
return err
}
}
}

outputBytes, err := cmd.CombinedOutput()
return nil
}

// FilesByCommand accepts git command and returns its result as a list of filepaths.
func (r *Repository) FilesByCommand(command string) ([]string, error) {
lines, err := r.Git.CmdLines(command)
if err != nil {
return nil, err
}

return strings.Split(string(outputBytes), "\n"), nil
return r.extractFiles(lines)
}

func (r *Repository) extractFiles(lines []string) ([]string, error) {
Expand Down Expand Up @@ -275,22 +289,3 @@ func (r *Repository) isFile(path string) (bool, error) {

return !stat.IsDir(), nil
}

func execGit(command string) (string, error) {
args := strings.Split(command, " ")
return execGitCmd(args...)
}

// execGitCmd executes git command with LEFTHOOK=0 in order
// to prevent calling subsequent lefthook hooks.
func execGitCmd(args ...string) (string, error) {
cmd := exec.Command(args[0], args[1:]...)
cmd.Env = append(os.Environ(), "LEFTHOOK=0")

out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}

return strings.TrimSpace(string(out)), nil
}
Loading

0 comments on commit 1d211d5

Please sign in to comment.