Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:
go-version-file: go.mod

- run: go mod download
- run: go vet ./...
- run: go build .
- run: go test ./... -race

release:
if: startsWith(github.ref, 'refs/tags/v')
Expand Down
129 changes: 129 additions & 0 deletions internal/gittest/gittest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Package gittest builds disposable, isolated git repositories for tests.
package gittest

import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)

// Repo is a real, disposable git repository used for tests.
type Repo struct {
Dir string
}

// NewBare creates a bare repository with a single initial commit on "main".
func NewBare(t testing.TB) *Repo {
t.Helper()
isolate(t)

dir := t.TempDir()
runIn(t, dir, "init", "--bare", "-b", "main")

// Bare repos can't be committed into directly, so seed via a throwaway clone.
seed := t.TempDir()
runIn(t, seed, "init", "-b", "main")
runIn(t, seed, "commit", "--allow-empty", "-m", "initial commit")
runIn(t, seed, "push", dir, "main")

return &Repo{Dir: dir}
}

// Clone clones r into a fresh temp directory, tracked as "origin".
func (r *Repo) Clone(t testing.TB) *Repo {
t.Helper()
isolate(t)

dir := t.TempDir()
runIn(t, dir, "clone", r.Dir, ".")

return &Repo{Dir: dir}
}

// Commit creates an empty commit with msg and returns its full SHA.
func (r *Repo) Commit(t testing.TB, msg string) string {
t.Helper()
runIn(t, r.Dir, "commit", "--allow-empty", "-m", msg)
return strings.TrimSpace(runIn(t, r.Dir, "rev-parse", "HEAD"))
}

// CreateBranch creates a new branch at HEAD without switching to it.
func (r *Repo) CreateBranch(t testing.TB, name string) {
t.Helper()
runIn(t, r.Dir, "branch", name)
}

// Checkout switches the working tree to the given branch.
func (r *Repo) Checkout(t testing.TB, name string) {
t.Helper()
runIn(t, r.Dir, "checkout", "--quiet", name)
}

// Push runs `git push` with the given args in r.
func (r *Repo) Push(t testing.TB, args ...string) {
t.Helper()
runIn(t, r.Dir, append([]string{"push"}, args...)...)
}

// Git runs an arbitrary git command in r and returns trimmed stdout.
func (r *Repo) Git(t testing.TB, args ...string) string {
t.Helper()
return strings.TrimSpace(runIn(t, r.Dir, args...))
}

// Chdir makes dir the process's cwd for the rest of the test.
func Chdir(t testing.TB, dir string) {
t.Helper()

orig, err := os.Getwd()
if err != nil {
t.Fatalf("gittest: os.Getwd: %v", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("gittest: os.Chdir(%s): %v", dir, err)
}
t.Cleanup(func() {
if err := os.Chdir(orig); err != nil {
t.Fatalf("gittest: restore os.Chdir(%s): %v", orig, err)
}
})
}

// Chdir makes r.Dir the process's cwd for the rest of the test.
func (r *Repo) Chdir(t testing.TB) {
t.Helper()
Chdir(t, r.Dir)
}

// isolate pins git's environment to per-test temp locations.
func isolate(t testing.TB) {
t.Helper()

home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
t.Setenv("GIT_AUTHOR_NAME", "git-sync tests")
t.Setenv("GIT_AUTHOR_EMAIL", "git-sync-tests@example.com")
t.Setenv("GIT_COMMITTER_NAME", "git-sync tests")
t.Setenv("GIT_COMMITTER_EMAIL", "git-sync-tests@example.com")
t.Setenv("GIT_TERMINAL_PROMPT", "0")
}

func runIn(t testing.TB, dir string, args ...string) string {
t.Helper()

var stdout, stderr strings.Builder
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
output := stdout.String() + stderr.String()
t.Fatalf("gittest: git %s (in %s): %v\n%s", strings.Join(args, " "), dir, err, output)
}
return stdout.String()
}
86 changes: 86 additions & 0 deletions internal/gittest/gittest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package gittest_test

import (
"os"
"path/filepath"
"testing"

"github.com/arbourd/git-sync/internal/gittest"
)

func TestNewBareAndClone(t *testing.T) {
remote := gittest.NewBare(t)
local := remote.Clone(t)

if got := local.Git(t, "rev-parse", "--abbrev-ref", "HEAD"); got != "main" {
t.Fatalf("current branch = %q, want %q", got, "main")
}
if got := local.Git(t, "remote"); got != "origin" {
t.Fatalf("remote = %q, want %q", got, "origin")
}
if got := local.Git(t, "symbolic-ref", "refs/remotes/origin/HEAD"); got != "refs/remotes/origin/main" {
t.Fatalf("origin/HEAD = %q, want %q", got, "refs/remotes/origin/main")
}
}

func TestCommitCreateBranchCheckout(t *testing.T) {
remote := gittest.NewBare(t)
local := remote.Clone(t)

local.CreateBranch(t, "feature")
local.Checkout(t, "feature")
sha := local.Commit(t, "feature work")

if got := local.Git(t, "rev-parse", "HEAD"); got != sha {
t.Fatalf("HEAD = %q, want %q", got, sha)
}
if got := local.Git(t, "rev-parse", "--abbrev-ref", "HEAD"); got != "feature" {
t.Fatalf("current branch = %q, want %q", got, "feature")
}
}

func TestPush(t *testing.T) {
remote := gittest.NewBare(t)
local := remote.Clone(t)

local.CreateBranch(t, "feature")
local.Checkout(t, "feature")
sha := local.Commit(t, "feature work")
local.Push(t, "origin", "feature")

if got := remote.Git(t, "rev-parse", "refs/heads/feature"); got != sha {
t.Fatalf("remote refs/heads/feature = %q, want %q", got, sha)
}
}

func TestChdir(t *testing.T) {
remote := gittest.NewBare(t)
local := remote.Clone(t)

orig, err := os.Getwd()
if err != nil {
t.Fatalf("os.Getwd: %v", err)
}

local.Chdir(t)

cwd, err := os.Getwd()
if err != nil {
t.Fatalf("os.Getwd: %v", err)
}

wantDir, err := filepath.EvalSymlinks(local.Dir)
if err != nil {
t.Fatalf("filepath.EvalSymlinks(%s): %v", local.Dir, err)
}
gotDir, err := filepath.EvalSymlinks(cwd)
if err != nil {
t.Fatalf("filepath.EvalSymlinks(%s): %v", cwd, err)
}
if gotDir != wantDir {
t.Fatalf("cwd = %q, want %q", gotDir, wantDir)
}
if orig == cwd {
t.Fatalf("cwd did not change from %q", orig)
}
}
29 changes: 10 additions & 19 deletions gitw/gitw.go → internal/gitw/gitw.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package gitw
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

Expand All @@ -15,6 +13,7 @@ import (
"github.com/ldez/go-git-cmd-wrapper/v2/types"
)

// NewRange resolves a and b to a Range.
func NewRange(a, b string) (*Range, error) {
// git rev-parse --quiet <a> <b>
out, err := git.RevParse(revparse.Quiet, revparse.Args(a, b))
Expand All @@ -30,15 +29,18 @@ func NewRange(a, b string) (*Range, error) {
return &Range{lines[0], lines[1]}, nil
}

// Range is a pair of resolved revisions.
type Range struct {
A string
B string
}

// IsIdentical reports whether A and B are the same revision.
func (r *Range) IsIdentical() bool {
return strings.EqualFold(r.A, r.B)
}

// IsAncestor reports whether A is an ancestor of B.
func (r *Range) IsAncestor() bool {
// git merge-base --is-ancestor <r.A> <r.B>
_, err := git.Raw("merge-base", func(g *types.Cmd) {
Expand All @@ -49,23 +51,13 @@ func (r *Range) IsAncestor() bool {
return err == nil
}

// HasFile reports whether the ref named by joining segments with "/" exists.
func HasFile(segments ...string) bool {
// The blessed way to resolve paths within git dir since Git 2.5.0
// git rev-parse -q --git-path <filepath.Join(segments...)>
out, err := git.RevParse(revparse.Quiet, revparse.GitPath(filepath.Join(segments...)))
if err != nil {
return false
}
ref := strings.Join(segments, "/")

lines := outputLines(out)
if len(lines) != 1 {
return false
}

if _, err := os.Stat(lines[0]); err != nil {
return false
}
return true
// git rev-parse --quiet --verify <ref>
_, err := git.RevParse(revparse.Quiet, revparse.Verify, revparse.Args(ref))
return err == nil
}

// RemoteFromHead gets the remote where the HEAD is.
Expand All @@ -85,8 +77,7 @@ func RemoteFromHead() (string, error) {
return match[1], nil
}

// BranchesWithRemotes uses the Git config to determine which branches also exist on the remotes.
// Returns a list of branches.
// BranchesWithRemotes returns a map of local branches to their remote.
func BranchesWithRemotes() (map[string]string, error) {
// git config --get-regexp 'branch.*.remote'
out, err := git.Config(config.GetRegexp("branch.*.remote", ""))
Expand Down
Loading