mirror of
https://github.com/grdl/git-get.git
synced 2026-02-10 11:24:17 +00:00
Add a run package responsible for running git commands
- Add better git error handling - Move repo helpers into a separate package
This commit is contained in:
@@ -83,7 +83,7 @@ func setMissingValues(cfg Gitconfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getOrDef(cfg Gitconfig, key string, def string) string {
|
func getOrDef(cfg Gitconfig, key string, def string) string {
|
||||||
if val := cfg.Get(key); val != "" {
|
if val := cfg.Get(fmt.Sprintf("%s.%s", GitgetPrefix, key)); val != "" {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
return def
|
return def
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import "os/exec"
|
import (
|
||||||
|
"git-get/pkg/run"
|
||||||
|
)
|
||||||
|
|
||||||
// ConfigGlobal represents a global gitconfig file.
|
// ConfigGlobal represents a global gitconfig file.
|
||||||
type ConfigGlobal struct{}
|
type ConfigGlobal struct{}
|
||||||
|
|
||||||
// Get reads a value from global gitconfig file. Returns empty string when key is missing.
|
// Get reads a value from global gitconfig file. Returns empty string when key is missing.
|
||||||
func (c *ConfigGlobal) Get(key string) string {
|
func (c *ConfigGlobal) Get(key string) string {
|
||||||
cmd := exec.Command("git", "config", "--global", key)
|
out, err := run.Git("config", "--global").AndCaptureLine()
|
||||||
out, err := cmd.Output()
|
|
||||||
|
|
||||||
// In case of error return an empty string, the missing value will fall back to a default.
|
// In case of error return an empty string, the missing value will fall back to a default.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
return out
|
||||||
return lines[0]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git-get/pkg/io"
|
||||||
|
"git-get/pkg/run"
|
||||||
|
"git-get/pkg/test"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cfgStub represents a gitconfig file but instead of using a global one, it creates a temporary git repo and uses its local gitconfig.
|
// cfgStub represents a gitconfig file but instead of using a global one, it creates a temporary git repo and uses its local gitconfig.
|
||||||
type cfgStub struct {
|
type cfgStub struct {
|
||||||
repo *testRepo
|
repo *test.Repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCfgStub(t *testing.T) *cfgStub {
|
func newCfgStub(t *testing.T) *cfgStub {
|
||||||
r := testRepoEmpty(t)
|
r := test.RepoEmpty(t)
|
||||||
return &cfgStub{
|
return &cfgStub{
|
||||||
repo: r,
|
repo: r,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cfgStub) Get(key string) string {
|
func (c *cfgStub) Get(key string) string {
|
||||||
cmd := gitCmd(c.repo.path, "config", "--local", key)
|
out, err := run.Git("config", "--local", key).OnRepo(c.repo.Path()).AndCaptureLine()
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
return out
|
||||||
return lines[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGitConfig(t *testing.T) {
|
func TestGitConfig(t *testing.T) {
|
||||||
@@ -73,7 +75,7 @@ func TestGitConfig(t *testing.T) {
|
|||||||
|
|
||||||
func makeConfigEmpty(t *testing.T) *cfgStub {
|
func makeConfigEmpty(t *testing.T) *cfgStub {
|
||||||
c := newCfgStub(t)
|
c := newCfgStub(t)
|
||||||
c.repo.writeFile(".git/config", "")
|
io.Write(path.Join(c.repo.Path(), dotgit, "config"), "")
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,7 @@ func makeConfigValid(t *testing.T) *cfgStub {
|
|||||||
[gitget]
|
[gitget]
|
||||||
host = github.com
|
host = github.com
|
||||||
`
|
`
|
||||||
c.repo.writeFile(".git/config", gitconfig)
|
io.Write(path.Join(c.repo.Path(), dotgit, "config"), gitconfig)
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
156
pkg/git/repo.go
156
pkg/git/repo.go
@@ -3,14 +3,10 @@ package git
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git-get/pkg/io"
|
"git-get/pkg/io"
|
||||||
|
"git-get/pkg/run"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -21,7 +17,19 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Repo represents a git repository on disk.
|
// Repo represents a git repository on disk.
|
||||||
type Repo struct {
|
type Repo interface {
|
||||||
|
Path() string
|
||||||
|
Branches() ([]string, error)
|
||||||
|
CurrentBranch() (string, error)
|
||||||
|
Fetch() error
|
||||||
|
Remote() (string, error)
|
||||||
|
Uncommitted() (int, error)
|
||||||
|
Untracked() (int, error)
|
||||||
|
Upstream(string) (string, error)
|
||||||
|
AheadBehind(string, string) (int, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type repo struct {
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,44 +43,38 @@ type CloneOpts struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open checks if given path can be accessed and returns a Repo instance pointing to it.
|
// Open checks if given path can be accessed and returns a Repo instance pointing to it.
|
||||||
func Open(path string) (*Repo, error) {
|
func Open(path string) (Repo, error) {
|
||||||
_, err := io.Exists(path)
|
_, err := io.Exists(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Repo{
|
return &repo{
|
||||||
path: path,
|
path: path,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone clones repository specified with CloneOpts.
|
// Clone clones repository specified with CloneOpts.
|
||||||
func Clone(opts *CloneOpts) (*Repo, error) {
|
func Clone(opts *CloneOpts) (Repo, error) {
|
||||||
// TODO: not sure if this check should be here
|
// TODO: not sure if this check should be here
|
||||||
if opts.IgnoreExisting {
|
if opts.IgnoreExisting {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"clone"}
|
runGit := run.Git("clone", opts.URL.String(), opts.Path)
|
||||||
|
|
||||||
if opts.Branch != "" {
|
if opts.Branch != "" {
|
||||||
args = append(args, "--branch", opts.Branch, "--single-branch")
|
runGit = run.Git("clone", "--branch", opts.Branch, "--single-branch", opts.URL.String(), opts.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if opts.Quiet {
|
if opts.Quiet {
|
||||||
args = append(args, "--quiet")
|
err = runGit.AndShutUp()
|
||||||
|
} else {
|
||||||
|
err = runGit.AndShow()
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, opts.URL.String())
|
|
||||||
args = append(args, opts.Path)
|
|
||||||
|
|
||||||
cmd := exec.Command("git", args...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "git clone failed")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
repo, err := Open(opts.Path)
|
repo, err := Open(opts.Path)
|
||||||
@@ -80,24 +82,21 @@ func Clone(opts *CloneOpts) (*Repo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch preforms a git fetch on all remotes
|
// Fetch preforms a git fetch on all remotes
|
||||||
func (r *Repo) Fetch() error {
|
func (r *repo) Fetch() error {
|
||||||
cmd := gitCmd(r.path, "fetch", "--all", "--quiet")
|
err := run.Git("fetch", "--all").OnRepo(r.path).AndShutUp()
|
||||||
return cmd.Run()
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncommitted returns the number of uncommitted files in the repository.
|
// Uncommitted returns the number of uncommitted files in the repository.
|
||||||
// Only tracked files are not counted.
|
// Only tracked files are not counted.
|
||||||
func (r *Repo) Uncommitted() (int, error) {
|
func (r *repo) Uncommitted() (int, error) {
|
||||||
cmd := gitCmd(r.path, "status", "--ignore-submodules", "--porcelain")
|
out, err := run.Git("status", "--ignore-submodules", "--porcelain").OnRepo(r.path).AndCaptureLines()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, cmdError(cmd, err)
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
|
||||||
count := 0
|
count := 0
|
||||||
for _, line := range lines {
|
for _, line := range out {
|
||||||
// Don't count lines with untracked files and empty lines.
|
// Don't count lines with untracked files and empty lines.
|
||||||
if !strings.HasPrefix(line, untracked) && strings.TrimSpace(line) != "" {
|
if !strings.HasPrefix(line, untracked) && strings.TrimSpace(line) != "" {
|
||||||
count++
|
count++
|
||||||
@@ -108,17 +107,14 @@ func (r *Repo) Uncommitted() (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Untracked returns the number of untracked files in the repository.
|
// Untracked returns the number of untracked files in the repository.
|
||||||
func (r *Repo) Untracked() (int, error) {
|
func (r *repo) Untracked() (int, error) {
|
||||||
cmd := gitCmd(r.path, "status", "--ignore-submodules", "--untracked-files=all", "--porcelain")
|
out, err := run.Git("status", "--ignore-submodules", "--untracked-files=all", "--porcelain").OnRepo(r.path).AndCaptureLines()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, cmdError(cmd, err)
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
|
||||||
count := 0
|
count := 0
|
||||||
for _, line := range lines {
|
for _, line := range out {
|
||||||
if strings.HasPrefix(line, untracked) {
|
if strings.HasPrefix(line, untracked) {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -129,69 +125,54 @@ func (r *Repo) Untracked() (int, error) {
|
|||||||
|
|
||||||
// CurrentBranch returns the short name currently checked-out branch for the repository.
|
// CurrentBranch returns the short name currently checked-out branch for the repository.
|
||||||
// If repo is in a detached head state, it will return "HEAD".
|
// If repo is in a detached head state, it will return "HEAD".
|
||||||
func (r *Repo) CurrentBranch() (string, error) {
|
func (r *repo) CurrentBranch() (string, error) {
|
||||||
cmd := gitCmd(r.path, "rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD")
|
out, err := run.Git("rev-parse", "--symbolic-full-name", "--abbrev-ref", "HEAD").OnRepo(r.path).AndCaptureLine()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", cmdError(cmd, err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
return out, nil
|
||||||
return lines[0], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Branches returns a list of local branches in the repository.
|
// Branches returns a list of local branches in the repository.
|
||||||
func (r *Repo) Branches() ([]string, error) {
|
func (r *repo) Branches() ([]string, error) {
|
||||||
cmd := gitCmd(r.path, "branch", "--format=%(refname:short)")
|
out, err := run.Git("branch", "--format=%(refname:short)").OnRepo(r.path).AndCaptureLines()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, cmdError(cmd, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
|
||||||
|
|
||||||
// TODO: Is detached head shown always on the first line? Maybe we don't need to iterate over everything.
|
// TODO: Is detached head shown always on the first line? Maybe we don't need to iterate over everything.
|
||||||
// Remove the line containing detached head.
|
// Remove the line containing detached head.
|
||||||
for i, line := range lines {
|
for i, line := range out {
|
||||||
if strings.Contains(line, "HEAD detached") {
|
if strings.Contains(line, "HEAD detached") {
|
||||||
lines = append(lines[:i], lines[i+1:]...)
|
out = append(out[:i], out[i+1:]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upstream returns the name of an upstream branch if a given branch is tracking one.
|
// Upstream returns the name of an upstream branch if a given branch is tracking one.
|
||||||
// Otherwise it returns an empty string.
|
// Otherwise it returns an empty string.
|
||||||
func (r *Repo) Upstream(branch string) (string, error) {
|
func (r *repo) Upstream(branch string) (string, error) {
|
||||||
cmd := gitCmd(r.path, "rev-parse", "--abbrev-ref", "--symbolic-full-name", fmt.Sprintf("%s@{upstream}", branch))
|
out, err := run.Git("rev-parse", "--abbrev-ref", "--symbolic-full-name", fmt.Sprintf("%s@{upstream}", branch)).OnRepo(r.path).AndCaptureLine()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
// TODO: no upstream will also throw an error.
|
// TODO: no upstream will also throw an error.
|
||||||
return "", nil //cmdError(cmd, err)
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
return out, nil
|
||||||
return lines[0], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AheadBehind returns the number of commits a given branch is ahead and/or behind the upstream.
|
// AheadBehind returns the number of commits a given branch is ahead and/or behind the upstream.
|
||||||
func (r *Repo) AheadBehind(branch string, upstream string) (int, int, error) {
|
func (r *repo) AheadBehind(branch string, upstream string) (int, int, error) {
|
||||||
cmd := gitCmd(r.path, "rev-list", "--left-right", "--count", fmt.Sprintf("%s...%s", branch, upstream))
|
out, err := run.Git("rev-list", "--left-right", "--count", fmt.Sprintf("%s...%s", branch, upstream)).OnRepo(r.path).AndCaptureLine()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, cmdError(cmd, err)
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
|
||||||
|
|
||||||
// rev-list --left-right --count output is separated by a tab
|
// rev-list --left-right --count output is separated by a tab
|
||||||
lr := strings.Split(lines[0], "\t")
|
lr := strings.Split(out, "\t")
|
||||||
|
|
||||||
ahead, err := strconv.Atoi(lr[0])
|
ahead, err := strconv.Atoi(lr[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -207,39 +188,18 @@ func (r *Repo) AheadBehind(branch string, upstream string) (int, int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remote returns URL of remote repository.
|
// Remote returns URL of remote repository.
|
||||||
func (r *Repo) Remote() (string, error) {
|
func (r *repo) Remote() (string, error) {
|
||||||
// https://stackoverflow.com/a/16880000/1085632
|
// https://stackoverflow.com/a/16880000/1085632
|
||||||
cmd := gitCmd(r.path, "ls-remote", "--get-url")
|
out, err := run.Git("ls-remote", "--get-url").OnRepo(r.path).AndCaptureLine()
|
||||||
|
|
||||||
out, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", cmdError(cmd, err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := lines(out)
|
|
||||||
|
|
||||||
// TODO: needs testing. What happens when there are more than 1 remotes?
|
// TODO: needs testing. What happens when there are more than 1 remotes?
|
||||||
return lines[0], nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path returns path to the repository.
|
// Path returns path to the repository.
|
||||||
func (r *Repo) Path() string {
|
func (r *repo) Path() string {
|
||||||
return r.path
|
return r.path
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitCmd(repoPath string, args ...string) *exec.Cmd {
|
|
||||||
args = append([]string{"--work-tree", repoPath, "--git-dir", path.Join(repoPath, dotgit)}, args...)
|
|
||||||
return exec.Command("git", args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func lines(output []byte) []string {
|
|
||||||
lines := strings.TrimSuffix(string(output), "\n")
|
|
||||||
return strings.Split(lines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func cmdError(cmd *exec.Cmd, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "%s failed: %+v", strings.Join(cmd.Args, " "), err) // Show which git command failed (skip "--work-tree and --gitdir flags")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
package git
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"git-get/pkg/io"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// testRepo embeds testing.T into a Repo instance to simplify creation of test repos.
|
|
||||||
// Any error thrown while creating a test repo will cause a t.Fatal call.
|
|
||||||
type testRepo struct {
|
|
||||||
*Repo
|
|
||||||
*testing.T
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this should be a method of a tempDir, not a repo
|
|
||||||
// Automatically remove test repo when the test is over
|
|
||||||
func (r *testRepo) cleanup() {
|
|
||||||
err := os.RemoveAll(r.path)
|
|
||||||
if err != nil {
|
|
||||||
r.T.Errorf("failed removing test repo directory %s", r.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoEmpty(t *testing.T) *testRepo {
|
|
||||||
dir, err := io.TempDir()
|
|
||||||
checkFatal(t, err)
|
|
||||||
|
|
||||||
r, err := Open(dir)
|
|
||||||
checkFatal(t, err)
|
|
||||||
|
|
||||||
tr := &testRepo{
|
|
||||||
Repo: r,
|
|
||||||
T: t,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Cleanup(tr.cleanup)
|
|
||||||
|
|
||||||
tr.init()
|
|
||||||
return tr
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithUntracked(t *testing.T) *testRepo {
|
|
||||||
r := testRepoEmpty(t)
|
|
||||||
r.writeFile("README.md", "I'm a readme file")
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithStaged(t *testing.T) *testRepo {
|
|
||||||
r := testRepoEmpty(t)
|
|
||||||
r.writeFile("README.md", "I'm a readme file")
|
|
||||||
r.stageFile("README.md")
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithCommit(t *testing.T) *testRepo {
|
|
||||||
r := testRepoEmpty(t)
|
|
||||||
r.writeFile("README.md", "I'm a readme file")
|
|
||||||
r.stageFile("README.md")
|
|
||||||
r.commit("Initial commit")
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithUncommittedAndUntracked(t *testing.T) *testRepo {
|
|
||||||
r := testRepoEmpty(t)
|
|
||||||
r.writeFile("README.md", "I'm a readme file")
|
|
||||||
r.stageFile("README.md")
|
|
||||||
r.commit("Initial commit")
|
|
||||||
r.writeFile("README.md", "These changes won't be committed")
|
|
||||||
r.writeFile("untracked.txt", "I'm untracked")
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithBranch(t *testing.T) *testRepo {
|
|
||||||
r := testRepoWithCommit(t)
|
|
||||||
r.branch("feature/branch")
|
|
||||||
r.checkout("feature/branch")
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithTag(t *testing.T) *testRepo {
|
|
||||||
r := testRepoWithCommit(t)
|
|
||||||
r.tag("v0.0.1")
|
|
||||||
r.checkout("v0.0.1")
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithBranchWithUpstream(t *testing.T) *testRepo {
|
|
||||||
origin := testRepoWithCommit(t)
|
|
||||||
origin.branch("feature/branch")
|
|
||||||
|
|
||||||
r := origin.clone()
|
|
||||||
r.checkout("feature/branch")
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithBranchWithoutUpstream(t *testing.T) *testRepo {
|
|
||||||
origin := testRepoWithCommit(t)
|
|
||||||
|
|
||||||
r := origin.clone()
|
|
||||||
r.branch("feature/branch")
|
|
||||||
r.checkout("feature/branch")
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithBranchAhead(t *testing.T) *testRepo {
|
|
||||||
origin := testRepoWithCommit(t)
|
|
||||||
origin.branch("feature/branch")
|
|
||||||
|
|
||||||
r := origin.clone()
|
|
||||||
r.checkout("feature/branch")
|
|
||||||
|
|
||||||
r.writeFile("local.new", "local.new")
|
|
||||||
r.stageFile("local.new")
|
|
||||||
r.commit("local.new")
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepoWithBranchBehind(t *testing.T) *testRepo {
|
|
||||||
origin := testRepoWithCommit(t)
|
|
||||||
origin.branch("feature/branch")
|
|
||||||
origin.checkout("feature/branch")
|
|
||||||
|
|
||||||
r := origin.clone()
|
|
||||||
r.checkout("feature/branch")
|
|
||||||
|
|
||||||
origin.writeFile("origin.new", "origin.new")
|
|
||||||
origin.stageFile("origin.new")
|
|
||||||
origin.commit("origin.new")
|
|
||||||
|
|
||||||
err := r.Fetch()
|
|
||||||
checkFatal(r.T, err)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns a repo with 2 commits ahead and 1 behind
|
|
||||||
func testRepoWithBranchAheadAndBehind(t *testing.T) *testRepo {
|
|
||||||
origin := testRepoWithCommit(t)
|
|
||||||
origin.branch("feature/branch")
|
|
||||||
origin.checkout("feature/branch")
|
|
||||||
|
|
||||||
r := origin.clone()
|
|
||||||
r.checkout("feature/branch")
|
|
||||||
|
|
||||||
origin.writeFile("origin.new", "origin.new")
|
|
||||||
origin.stageFile("origin.new")
|
|
||||||
origin.commit("origin.new")
|
|
||||||
|
|
||||||
r.writeFile("local.new", "local.new")
|
|
||||||
r.stageFile("local.new")
|
|
||||||
r.commit("local.new")
|
|
||||||
|
|
||||||
r.writeFile("local.new2", "local.new2")
|
|
||||||
r.stageFile("local.new2")
|
|
||||||
r.commit("local.new2")
|
|
||||||
|
|
||||||
err := r.Fetch()
|
|
||||||
checkFatal(r.T, err)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) writeFile(filename string, content string) {
|
|
||||||
path := path.Join(r.path, filename)
|
|
||||||
err := io.Write(path, content)
|
|
||||||
checkFatal(r.T, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) init() {
|
|
||||||
cmd := exec.Command("git", "init", "--quiet", r.path)
|
|
||||||
runGitCmd(r.T, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) stageFile(path string) {
|
|
||||||
cmd := gitCmd(r.path, "add", path)
|
|
||||||
runGitCmd(r.T, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) commit(msg string) {
|
|
||||||
cmd := gitCmd(r.path, "commit", "-m", fmt.Sprintf("%q", msg), "--author=\"user <user@example.com>\"")
|
|
||||||
runGitCmd(r.T, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) branch(name string) {
|
|
||||||
cmd := gitCmd(r.path, "branch", name)
|
|
||||||
runGitCmd(r.T, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) tag(name string) {
|
|
||||||
cmd := gitCmd(r.path, "tag", "-a", name, "-m", name)
|
|
||||||
runGitCmd(r.T, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) checkout(name string) {
|
|
||||||
cmd := gitCmd(r.path, "checkout", name)
|
|
||||||
runGitCmd(r.T, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *testRepo) clone() *testRepo {
|
|
||||||
dir, err := io.TempDir()
|
|
||||||
checkFatal(r.T, err)
|
|
||||||
|
|
||||||
url, err := url.Parse(fmt.Sprintf("file://%s/.git", r.path))
|
|
||||||
checkFatal(r.T, err)
|
|
||||||
|
|
||||||
opts := &CloneOpts{
|
|
||||||
URL: url,
|
|
||||||
Quiet: true,
|
|
||||||
Path: dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := Clone(opts)
|
|
||||||
checkFatal(r.T, err)
|
|
||||||
|
|
||||||
tr := &testRepo{
|
|
||||||
Repo: repo,
|
|
||||||
T: r.T,
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.T.Cleanup(tr.cleanup)
|
|
||||||
return tr
|
|
||||||
}
|
|
||||||
|
|
||||||
func runGitCmd(t *testing.T, cmd *exec.Cmd) {
|
|
||||||
err := cmd.Run()
|
|
||||||
checkFatal(t, cmdError(cmd, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkFatal(t *testing.T, err error) {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git-get/pkg/io"
|
"git-get/pkg/io"
|
||||||
|
"git-get/pkg/test"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -17,39 +18,39 @@ func TestOpen(t *testing.T) {
|
|||||||
func TestUncommitted(t *testing.T) {
|
func TestUncommitted(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
repoMaker func(*testing.T) *testRepo
|
repoMaker func(*testing.T) *test.Repo
|
||||||
want int
|
want int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
repoMaker: testRepoEmpty,
|
repoMaker: test.RepoEmpty,
|
||||||
want: 0,
|
want: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single untracked",
|
name: "single untracked",
|
||||||
repoMaker: testRepoWithUntracked,
|
repoMaker: test.RepoWithUntracked,
|
||||||
want: 0,
|
want: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single tracked ",
|
name: "single tracked ",
|
||||||
repoMaker: testRepoWithStaged,
|
repoMaker: test.RepoWithStaged,
|
||||||
want: 1,
|
want: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "committed",
|
name: "committed",
|
||||||
repoMaker: testRepoWithCommit,
|
repoMaker: test.RepoWithCommit,
|
||||||
want: 0,
|
want: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "untracked and uncommitted",
|
name: "untracked and uncommitted",
|
||||||
repoMaker: testRepoWithUncommittedAndUntracked,
|
repoMaker: test.RepoWithUncommittedAndUntracked,
|
||||||
want: 1,
|
want: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
r := test.repoMaker(t)
|
r, _ := Open(test.repoMaker(t).Path())
|
||||||
got, err := r.Uncommitted()
|
got, err := r.Uncommitted()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,39 +66,39 @@ func TestUncommitted(t *testing.T) {
|
|||||||
func TestUntracked(t *testing.T) {
|
func TestUntracked(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
repoMaker func(*testing.T) *testRepo
|
repoMaker func(*testing.T) *test.Repo
|
||||||
want int
|
want int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
repoMaker: testRepoEmpty,
|
repoMaker: test.RepoEmpty,
|
||||||
want: 0,
|
want: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single untracked",
|
name: "single untracked",
|
||||||
repoMaker: testRepoWithUntracked,
|
repoMaker: test.RepoWithUntracked,
|
||||||
want: 0,
|
want: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "single tracked ",
|
name: "single tracked ",
|
||||||
repoMaker: testRepoWithStaged,
|
repoMaker: test.RepoWithStaged,
|
||||||
want: 1,
|
want: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "committed",
|
name: "committed",
|
||||||
repoMaker: testRepoWithCommit,
|
repoMaker: test.RepoWithCommit,
|
||||||
want: 0,
|
want: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "untracked and uncommitted",
|
name: "untracked and uncommitted",
|
||||||
repoMaker: testRepoWithUncommittedAndUntracked,
|
repoMaker: test.RepoWithUncommittedAndUntracked,
|
||||||
want: 1,
|
want: 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
r := test.repoMaker(t)
|
r, _ := Open(test.repoMaker(t).Path())
|
||||||
got, err := r.Uncommitted()
|
got, err := r.Uncommitted()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,7 +115,7 @@ func TestUntracked(t *testing.T) {
|
|||||||
func TestCurrentBranch(t *testing.T) {
|
func TestCurrentBranch(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
repoMaker func(*testing.T) *testRepo
|
repoMaker func(*testing.T) *test.Repo
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
// TODO: maybe add wantErr to check if error is returned correctly?
|
// TODO: maybe add wantErr to check if error is returned correctly?
|
||||||
@@ -125,24 +126,24 @@ func TestCurrentBranch(t *testing.T) {
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
name: "only master branch",
|
name: "only master branch",
|
||||||
repoMaker: testRepoWithCommit,
|
repoMaker: test.RepoWithCommit,
|
||||||
want: master,
|
want: master,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "checked out new branch",
|
name: "checked out new branch",
|
||||||
repoMaker: testRepoWithBranch,
|
repoMaker: test.RepoWithBranch,
|
||||||
want: "feature/branch",
|
want: "feature/branch",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "checked out new tag",
|
name: "checked out new tag",
|
||||||
repoMaker: testRepoWithTag,
|
repoMaker: test.RepoWithTag,
|
||||||
want: head,
|
want: head,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
r := test.repoMaker(t)
|
r, _ := Open(test.repoMaker(t).Path())
|
||||||
got, err := r.CurrentBranch()
|
got, err := r.CurrentBranch()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -158,34 +159,34 @@ func TestCurrentBranch(t *testing.T) {
|
|||||||
func TestBranches(t *testing.T) {
|
func TestBranches(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
repoMaker func(*testing.T) *testRepo
|
repoMaker func(*testing.T) *test.Repo
|
||||||
want []string
|
want []string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
repoMaker: testRepoEmpty,
|
repoMaker: test.RepoEmpty,
|
||||||
want: []string{""},
|
want: []string{""},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only master branch",
|
name: "only master branch",
|
||||||
repoMaker: testRepoWithCommit,
|
repoMaker: test.RepoWithCommit,
|
||||||
want: []string{"master"},
|
want: []string{"master"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "new branch",
|
name: "new branch",
|
||||||
repoMaker: testRepoWithBranch,
|
repoMaker: test.RepoWithBranch,
|
||||||
want: []string{"feature/branch", "master"},
|
want: []string{"feature/branch", "master"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "checked out new tag",
|
name: "checked out new tag",
|
||||||
repoMaker: testRepoWithTag,
|
repoMaker: test.RepoWithTag,
|
||||||
want: []string{"master"},
|
want: []string{"master"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
r := test.repoMaker(t)
|
r, _ := Open(test.repoMaker(t).Path())
|
||||||
got, err := r.Branches()
|
got, err := r.Branches()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,38 +202,38 @@ func TestBranches(t *testing.T) {
|
|||||||
func TestUpstream(t *testing.T) {
|
func TestUpstream(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
repoMaker func(*testing.T) *testRepo
|
repoMaker func(*testing.T) *test.Repo
|
||||||
branch string
|
branch string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
repoMaker: testRepoEmpty,
|
repoMaker: test.RepoEmpty,
|
||||||
branch: "master",
|
branch: "master",
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
// TODO: add wantErr
|
// TODO: add wantErr
|
||||||
{
|
{
|
||||||
name: "wrong branch name",
|
name: "wrong branch name",
|
||||||
repoMaker: testRepoWithCommit,
|
repoMaker: test.RepoWithCommit,
|
||||||
branch: "wrong_branch_name",
|
branch: "wrong_branch_name",
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "master with upstream",
|
name: "master with upstream",
|
||||||
repoMaker: testRepoWithBranchWithUpstream,
|
repoMaker: test.RepoWithBranchWithUpstream,
|
||||||
branch: "master",
|
branch: "master",
|
||||||
want: "origin/master",
|
want: "origin/master",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "branch with upstream",
|
name: "branch with upstream",
|
||||||
repoMaker: testRepoWithBranchWithUpstream,
|
repoMaker: test.RepoWithBranchWithUpstream,
|
||||||
branch: "feature/branch",
|
branch: "feature/branch",
|
||||||
want: "origin/feature/branch",
|
want: "origin/feature/branch",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "branch without upstream",
|
name: "branch without upstream",
|
||||||
repoMaker: testRepoWithBranchWithoutUpstream,
|
repoMaker: test.RepoWithBranchWithoutUpstream,
|
||||||
branch: "feature/branch",
|
branch: "feature/branch",
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
@@ -240,7 +241,7 @@ func TestUpstream(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
r := test.repoMaker(t)
|
r, _ := Open(test.repoMaker(t).Path())
|
||||||
got, _ := r.Upstream(test.branch)
|
got, _ := r.Upstream(test.branch)
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
@@ -257,32 +258,32 @@ func TestUpstream(t *testing.T) {
|
|||||||
func TestAheadBehind(t *testing.T) {
|
func TestAheadBehind(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
repoMaker func(*testing.T) *testRepo
|
repoMaker func(*testing.T) *test.Repo
|
||||||
branch string
|
branch string
|
||||||
want []int
|
want []int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "fresh clone",
|
name: "fresh clone",
|
||||||
repoMaker: testRepoWithBranchWithUpstream,
|
repoMaker: test.RepoWithBranchWithUpstream,
|
||||||
branch: "master",
|
branch: "master",
|
||||||
want: []int{0, 0},
|
want: []int{0, 0},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "branch ahead",
|
name: "branch ahead",
|
||||||
repoMaker: testRepoWithBranchAhead,
|
repoMaker: test.RepoWithBranchAhead,
|
||||||
branch: "feature/branch",
|
branch: "feature/branch",
|
||||||
want: []int{1, 0},
|
want: []int{1, 0},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "branch behind",
|
name: "branch behind",
|
||||||
repoMaker: testRepoWithBranchBehind,
|
repoMaker: test.RepoWithBranchBehind,
|
||||||
branch: "feature/branch",
|
branch: "feature/branch",
|
||||||
want: []int{0, 1},
|
want: []int{0, 1},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "branch ahead and behind",
|
name: "branch ahead and behind",
|
||||||
repoMaker: testRepoWithBranchAheadAndBehind,
|
repoMaker: test.RepoWithBranchAheadAndBehind,
|
||||||
branch: "feature/branch",
|
branch: "feature/branch",
|
||||||
want: []int{2, 1},
|
want: []int{2, 1},
|
||||||
},
|
},
|
||||||
@@ -290,7 +291,7 @@ func TestAheadBehind(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
r := test.repoMaker(t)
|
r, _ := Open(test.repoMaker(t).Path())
|
||||||
upstream, err := r.Upstream(test.branch)
|
upstream, err := r.Upstream(test.branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("got error %q", err)
|
t.Errorf("got error %q", err)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func Load(path string) *Loaded {
|
|||||||
return loaded
|
return loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBranches(r *git.Repo) (map[string]string, []error) {
|
func loadBranches(r git.Repo) (map[string]string, []error) {
|
||||||
statuses := make(map[string]string)
|
statuses := make(map[string]string)
|
||||||
errors := make([]error, 0)
|
errors := make([]error, 0)
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ func loadBranches(r *git.Repo) (map[string]string, []error) {
|
|||||||
return statuses, errors
|
return statuses, errors
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBranchStatus(r *git.Repo, branch string) (string, error) {
|
func loadBranchStatus(r git.Repo, branch string) (string, error) {
|
||||||
upstream, err := r.Upstream(branch)
|
upstream, err := r.Upstream(branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -105,7 +105,7 @@ func loadBranchStatus(r *git.Repo, branch string) (string, error) {
|
|||||||
return strings.Join(res, " "), nil
|
return strings.Join(res, " "), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadWorkTree(r *git.Repo) (string, error) {
|
func loadWorkTree(r git.Repo) (string, error) {
|
||||||
uncommitted, err := r.Uncommitted()
|
uncommitted, err := r.Uncommitted()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
125
pkg/run/run.go
Normal file
125
pkg/run/run.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Package run provides methods for running git command and capturing their output and errors
|
||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
pathpkg "path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cmd represents a git command.
|
||||||
|
// The command is executed by chaining functions: Git() + optional OnRepo() + output specifier.
|
||||||
|
// This way the function chain reads more naturally.
|
||||||
|
//
|
||||||
|
// Examples of different compositions:
|
||||||
|
//
|
||||||
|
// - run.Git("clone", <URL>).AndShow()
|
||||||
|
// means running "git clone <URL>" and printing the progress into stdout
|
||||||
|
//
|
||||||
|
// - run.Git("branch","-a").OnRepo(<REPO>).AndCaptureLines()
|
||||||
|
// means running "git branch -a" inside <REPO> and returning a slice of branch names
|
||||||
|
//
|
||||||
|
// - run.Git("pull").OnRepo(<REPO>).AndShutUp()
|
||||||
|
// means running "git pull" inside <REPO> and not printing any output
|
||||||
|
type Cmd struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git creates a git command with given arguments.
|
||||||
|
func Git(args ...string) *Cmd {
|
||||||
|
return &Cmd{
|
||||||
|
cmd: exec.Command("git", args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRepo makes the command run inside a given repository path. Otherwise the command is run outside of any repository.
|
||||||
|
// Commands like "git clone" or "git config --global" don't have to (or shouldn't in some cases) be run inside a repo.
|
||||||
|
func (c *Cmd) OnRepo(path string) *Cmd {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
insert := []string{"--work-tree", path, "--git-dir", pathpkg.Join(path, ".git")}
|
||||||
|
// Insert into the args slice after the 1st element (https://github.com/golang/go/wiki/SliceTricks#insert)
|
||||||
|
c.cmd.Args = append(c.cmd.Args[:1], append(insert, c.cmd.Args[1:]...)...)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndCaptureLines executes the command and returns its output as a slice of lines.
|
||||||
|
func (c *Cmd) AndCaptureLines() ([]string, error) {
|
||||||
|
errStream := &bytes.Buffer{}
|
||||||
|
c.cmd.Stderr = errStream
|
||||||
|
|
||||||
|
out, err := c.cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, &GitError{errStream, c.cmd.Args, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := lines(out)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return []string{""}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndCaptureLine executes the command and returns the first line of its output.
|
||||||
|
func (c *Cmd) AndCaptureLine() (string, error) {
|
||||||
|
lines, err := c.AndCaptureLines()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return lines[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndShow executes the command and prints its output into standard output.
|
||||||
|
func (c *Cmd) AndShow() error {
|
||||||
|
c.cmd.Stdout = os.Stdout
|
||||||
|
|
||||||
|
errStream := &bytes.Buffer{}
|
||||||
|
c.cmd.Stderr = errStream
|
||||||
|
|
||||||
|
err := c.cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return &GitError{errStream, c.cmd.Args, err}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AndShutUp executes the command and doesn't return or show any output.
|
||||||
|
func (c *Cmd) AndShutUp() error {
|
||||||
|
c.cmd.Stdout = nil
|
||||||
|
|
||||||
|
errStream := &bytes.Buffer{}
|
||||||
|
c.cmd.Stderr = errStream
|
||||||
|
|
||||||
|
err := c.cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return &GitError{errStream, c.cmd.Args, err}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitError provides more visibility into why an git command had failed.
|
||||||
|
type GitError struct {
|
||||||
|
Stderr *bytes.Buffer
|
||||||
|
Args []string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e GitError) Error() string {
|
||||||
|
msg := e.Stderr.String()
|
||||||
|
if msg != "" && !strings.HasSuffix(msg, "\n") {
|
||||||
|
msg += "\n"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%q: %s", msg, strings.Join(e.Args, " "), e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lines(output []byte) []string {
|
||||||
|
lines := strings.TrimSuffix(string(output), "\n")
|
||||||
|
return strings.Split(lines, "\n")
|
||||||
|
}
|
||||||
73
pkg/test/helpers.go
Normal file
73
pkg/test/helpers.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git-get/pkg/io"
|
||||||
|
"git-get/pkg/run"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Repo) writeFile(filename string, content string) {
|
||||||
|
path := path.Join(r.path, filename)
|
||||||
|
err := io.Write(path, content)
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) init() {
|
||||||
|
err := run.Git("init", "--quiet", r.path).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) stageFile(path string) {
|
||||||
|
err := run.Git("add", path).OnRepo(r.path).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) commit(msg string) {
|
||||||
|
err := run.Git("commit", "-m", fmt.Sprintf("%q", msg), "--author=\"user <user@example.com>\"").OnRepo(r.path).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) branch(name string) {
|
||||||
|
err := run.Git("branch", name).OnRepo(r.path).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) tag(name string) {
|
||||||
|
err := run.Git("tag", "-a", name, "-m", name).OnRepo(r.path).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) checkout(name string) {
|
||||||
|
err := run.Git("checkout", name).OnRepo(r.path).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) clone() *Repo {
|
||||||
|
dir, err := io.TempDir()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("file://%s/.git", r.path)
|
||||||
|
err = run.Git("clone", url, dir).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
|
||||||
|
clone := &Repo{
|
||||||
|
path: dir,
|
||||||
|
t: r.t,
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.t.Cleanup(r.cleanup)
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) fetch() {
|
||||||
|
err := run.Git("fetch", "--all").OnRepo(r.path).AndShutUp()
|
||||||
|
checkFatal(r.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFatal(t *testing.T, err error) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed making test repo: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
168
pkg/test/testrepos.go
Normal file
168
pkg/test/testrepos.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git-get/pkg/io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repo represents a test repository.
|
||||||
|
// It embeds testing.T so that any error thrown while creating a test repo will cause a t.Fatal call.
|
||||||
|
type Repo struct {
|
||||||
|
path string
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repo) Path() string {
|
||||||
|
return r.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this should be a method of a tempDir, not a repo
|
||||||
|
// Automatically remove test repo when the test is over
|
||||||
|
func (r *Repo) cleanup() {
|
||||||
|
err := os.RemoveAll(r.path)
|
||||||
|
if err != nil {
|
||||||
|
r.t.Errorf("failed removing test repo directory %s", r.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoEmpty(t *testing.T) *Repo {
|
||||||
|
dir, err := io.TempDir()
|
||||||
|
checkFatal(t, err)
|
||||||
|
|
||||||
|
r := &Repo{
|
||||||
|
path: dir,
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(r.cleanup)
|
||||||
|
|
||||||
|
r.init()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithUntracked(t *testing.T) *Repo {
|
||||||
|
r := RepoEmpty(t)
|
||||||
|
r.writeFile("README.md", "I'm a readme file")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithStaged(t *testing.T) *Repo {
|
||||||
|
r := RepoEmpty(t)
|
||||||
|
r.writeFile("README.md", "I'm a readme file")
|
||||||
|
r.stageFile("README.md")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithCommit(t *testing.T) *Repo {
|
||||||
|
r := RepoEmpty(t)
|
||||||
|
r.writeFile("README.md", "I'm a readme file")
|
||||||
|
r.stageFile("README.md")
|
||||||
|
r.commit("Initial commit")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithUncommittedAndUntracked(t *testing.T) *Repo {
|
||||||
|
r := RepoEmpty(t)
|
||||||
|
r.writeFile("README.md", "I'm a readme file")
|
||||||
|
r.stageFile("README.md")
|
||||||
|
r.commit("Initial commit")
|
||||||
|
r.writeFile("README.md", "These changes won't be committed")
|
||||||
|
r.writeFile("untracked.txt", "I'm untracked")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithBranch(t *testing.T) *Repo {
|
||||||
|
r := RepoWithCommit(t)
|
||||||
|
r.branch("feature/branch")
|
||||||
|
r.checkout("feature/branch")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithTag(t *testing.T) *Repo {
|
||||||
|
r := RepoWithCommit(t)
|
||||||
|
r.tag("v0.0.1")
|
||||||
|
r.checkout("v0.0.1")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithBranchWithUpstream(t *testing.T) *Repo {
|
||||||
|
origin := RepoWithCommit(t)
|
||||||
|
origin.branch("feature/branch")
|
||||||
|
|
||||||
|
r := origin.clone()
|
||||||
|
r.checkout("feature/branch")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithBranchWithoutUpstream(t *testing.T) *Repo {
|
||||||
|
origin := RepoWithCommit(t)
|
||||||
|
|
||||||
|
r := origin.clone()
|
||||||
|
r.branch("feature/branch")
|
||||||
|
r.checkout("feature/branch")
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithBranchAhead(t *testing.T) *Repo {
|
||||||
|
origin := RepoWithCommit(t)
|
||||||
|
origin.branch("feature/branch")
|
||||||
|
|
||||||
|
r := origin.clone()
|
||||||
|
r.checkout("feature/branch")
|
||||||
|
|
||||||
|
r.writeFile("local.new", "local.new")
|
||||||
|
r.stageFile("local.new")
|
||||||
|
r.commit("local.new")
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func RepoWithBranchBehind(t *testing.T) *Repo {
|
||||||
|
origin := RepoWithCommit(t)
|
||||||
|
origin.branch("feature/branch")
|
||||||
|
origin.checkout("feature/branch")
|
||||||
|
|
||||||
|
r := origin.clone()
|
||||||
|
r.checkout("feature/branch")
|
||||||
|
|
||||||
|
origin.writeFile("origin.new", "origin.new")
|
||||||
|
origin.stageFile("origin.new")
|
||||||
|
origin.commit("origin.new")
|
||||||
|
|
||||||
|
r.fetch()
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a repo with 2 commits ahead and 1 behind
|
||||||
|
func RepoWithBranchAheadAndBehind(t *testing.T) *Repo {
|
||||||
|
origin := RepoWithCommit(t)
|
||||||
|
origin.branch("feature/branch")
|
||||||
|
origin.checkout("feature/branch")
|
||||||
|
|
||||||
|
r := origin.clone()
|
||||||
|
r.checkout("feature/branch")
|
||||||
|
|
||||||
|
origin.writeFile("origin.new", "origin.new")
|
||||||
|
origin.stageFile("origin.new")
|
||||||
|
origin.commit("origin.new")
|
||||||
|
|
||||||
|
r.writeFile("local.new", "local.new")
|
||||||
|
r.stageFile("local.new")
|
||||||
|
r.commit("local.new")
|
||||||
|
|
||||||
|
r.writeFile("local.new2", "local.new2")
|
||||||
|
r.stageFile("local.new2")
|
||||||
|
r.commit("local.new2")
|
||||||
|
|
||||||
|
r.fetch()
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user