6
0
mirror of https://github.com/grdl/git-get.git synced 2026-02-04 17:24:49 +00:00

Add branch status and tests

This commit is contained in:
Grzegorz Dlugoszewski
2020-05-27 20:30:04 +02:00
parent fd49a9e41d
commit 0b371341e7
8 changed files with 338 additions and 31 deletions

1
go.mod
View File

@@ -5,7 +5,6 @@ go 1.14
require ( require (
github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-billy/v5 v5.0.0
github.com/go-git/go-git/v5 v5.1.0 github.com/go-git/go-git/v5 v5.1.0
github.com/libgit2/git2go/v30 v30.0.3
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.0.0 github.com/spf13/cobra v1.0.0

View File

@@ -2,11 +2,13 @@ package new
import ( import (
"io/ioutil" "io/ioutil"
urlpkg "net/url" pkgurl "net/url"
"os" "os"
"testing" "testing"
"time" "time"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@@ -15,7 +17,8 @@ import (
type TestRepo struct { type TestRepo struct {
Repo *git.Repository Repo *git.Repository
URL *urlpkg.URL Path string
URL *pkgurl.URL
t *testing.T t *testing.T
} }
@@ -25,11 +28,12 @@ func NewRepoEmpty(t *testing.T) *TestRepo {
repo, err := git.PlainInit(dir, false) repo, err := git.PlainInit(dir, false)
checkFatal(t, err) checkFatal(t, err)
url, err := urlpkg.Parse("file://" + dir) url, err := ParseURL("file://" + dir)
checkFatal(t, err) checkFatal(t, err)
return &TestRepo{ return &TestRepo{
Repo: repo, Repo: repo,
Path: dir,
URL: url, URL: url,
t: t, t: t,
} }
@@ -108,20 +112,33 @@ func (r *TestRepo) NewCommit(msg string) {
checkFatal(r.t, errors.Wrap(err, "Failed creating commit")) checkFatal(r.t, errors.Wrap(err, "Failed creating commit"))
} }
// func (r *TestRepo) NewBranch(name string) {
//func createBranch(t *testing.T, repo *git.Repository, name string) *git.Branch { head, err := r.Repo.Head()
// head, err := repo.Head() checkFatal(r.t, err)
// checkFatal(t, errors.Wrap(err, "Failed getting repo head"))
// ref := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), head.Hash())
// commit, err := repo.LookupCommit(head.Target())
// checkFatal(t, errors.Wrap(err, "Failed getting commit id from head")) err = r.Repo.Storer.SetReference(ref)
// checkFatal(r.t, err)
// branch, err := repo.CreateBranch(name, commit, false) }
// checkFatal(t, errors.Wrap(err, "Failed creating branch"))
// func (r *TestRepo) Clone() *TestRepo {
// return branch dir := NewTempDir(r.t)
//}
// repo, err := CloneRepo(r.URL, dir, true)
checkFatal(r.t, err)
url, err := ParseURL("file://" + dir)
checkFatal(r.t, err)
return &TestRepo{
Repo: repo.repo,
Path: dir,
URL: url,
t: r.t,
}
}
//func checkoutBranch(t *testing.T, repo *git.Repository, name string) { //func checkoutBranch(t *testing.T, repo *git.Repository, name string) {
// branch, err := repo.LookupBranch(name, git.BranchAll) // branch, err := repo.LookupBranch(name, git.BranchAll)
// //

View File

@@ -39,8 +39,25 @@ func CloneRepo(url *url.URL, path string, quiet bool) (r *Repo, err error) {
return nil, errors.Wrap(err, "Failed cloning repo") return nil, errors.Wrap(err, "Failed cloning repo")
} }
r = &Repo{ return newRepo(repo), nil
repo: repo, }
}
return r, nil func OpenRepo(path string) (r *Repo, err error) {
repo, err := git.PlainOpen(path)
if err != nil {
return nil, errors.Wrap(err, "Failed opening repo")
}
return newRepo(repo), nil
}
func newRepo(repo *git.Repository) *Repo {
return &Repo{
repo: repo,
Status: &RepoStatus{
HasUntrackedFiles: false,
HasUncommittedChanges: false,
Branches: make(map[string]*BranchStatus),
},
}
} }

View File

@@ -6,8 +6,8 @@ import (
func TestRepoClone(t *testing.T) { func TestRepoClone(t *testing.T) {
origin := NewRepoWithCommit(t) origin := NewRepoWithCommit(t)
path := NewTempDir(t)
path := NewTempDir(t)
repo, err := CloneRepo(origin.URL, path, true) repo, err := CloneRepo(origin.URL, path, true)
checkFatal(t, err) checkFatal(t, err)

View File

@@ -2,23 +2,28 @@ package new
import ( import (
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type RepoStatus struct { type RepoStatus struct {
HasUntrackedFiles bool HasUntrackedFiles bool
HasUncommittedChanges bool HasUncommittedChanges bool
Branches map[string]BranchStatus Branches map[string]*BranchStatus
} }
type BranchStatus struct { type BranchStatus struct {
Name string Name string
IsRemote bool Upstream *Upstream
HasUpstream bool NeedsPull bool
NeedsPull bool NeedsPush bool
NeedsPush bool Ahead int
Ahead int Behind int
Behind int }
type Upstream struct {
Remote string
Branch string
} }
func (r *Repo) LoadStatus() error { func (r *Repo) LoadStatus() error {
@@ -34,6 +39,12 @@ func (r *Repo) LoadStatus() error {
r.Status.HasUncommittedChanges = !status.IsClean() r.Status.HasUncommittedChanges = !status.IsClean()
r.Status.HasUntrackedFiles = hasUntracked(status) r.Status.HasUntrackedFiles = hasUntracked(status)
err = r.LoadBranchesStatus()
if err != nil {
return err
}
return nil return nil
} }
@@ -47,3 +58,73 @@ func hasUntracked(status git.Status) bool {
return false return false
} }
func (r *Repo) LoadBranchesStatus() error {
iter, err := r.repo.Branches()
if err != nil {
return errors.Wrap(err, "Failed getting branches iterator")
}
err = iter.ForEach(func(reference *plumbing.Reference) error {
bs, err := r.newBranchStatus(reference.Name().Short())
if err != nil {
return err
}
r.Status.Branches[bs.Name] = bs
return nil
})
if err != nil {
return errors.Wrap(err, "Failed iterating over branches")
}
return nil
}
func (r *Repo) newBranchStatus(branch string) (*BranchStatus, error) {
upstream, err := r.upstream(branch)
if err != nil {
return nil, err
}
return &BranchStatus{
Name: branch,
Upstream: upstream,
}, nil
}
// upstream finds if a given branch tracks an upstream.
// Returns found upstream or nil if branch doesn't track an upstream.
//
// Information about upstream is taken from .git/config file.
// If a branch has an upstream, there's a [branch] section in the file with two fields:
// "remote" - name of the remote containing upstreamn branch (or "." if upstream is a local branch)
// "merge" - full ref name of the upstream branch (eg, ref/heads/master)
func (r *Repo) upstream(branch string) (*Upstream, error) {
cfg, err := r.repo.Config()
if err != nil {
return nil, errors.Wrap(err, "Failed getting repo config")
}
// Find our branch in "branch" config sections
bcfg := cfg.Branches[branch]
if bcfg == nil {
return nil, nil
}
remote := bcfg.Remote
if remote == "" {
return nil, nil
}
// TODO: check if this should be short or full ref name
merge := bcfg.Merge.Short()
if merge == "" {
return nil, nil
}
return &Upstream{
Remote: remote,
Branch: merge,
}, nil
}

44
new/status_test.go Normal file
View File

@@ -0,0 +1,44 @@
package new
import "testing"
func TestBranchStatusLocal(t *testing.T) {
tr := NewRepoWithCommit(t)
tr.NewBranch("branch")
repo, err := OpenRepo(tr.Path)
checkFatal(t, err)
err = repo.LoadStatus()
checkFatal(t, err)
if repo.Status.Branches["master"].Upstream != nil {
t.Errorf("'master' branch should not have an upstream")
}
if repo.Status.Branches["branch"].Upstream != nil {
t.Errorf("'branch' branch should not have an upstream")
}
}
func TestBranchStatusCloned(t *testing.T) {
origin := NewRepoWithCommit(t)
clone := origin.Clone()
clone.NewBranch("local")
repo, err := OpenRepo(clone.Path)
checkFatal(t, err)
err = repo.LoadStatus()
checkFatal(t, err)
if repo.Status.Branches["master"].Upstream == nil {
t.Errorf("'master' branch should have an upstream")
}
if repo.Status.Branches["local"].Upstream != nil {
t.Errorf("'local' branch should not have an upstream")
}
}

68
new/url.go Normal file
View File

@@ -0,0 +1,68 @@
package new
import (
urlpkg "net/url"
"path"
"regexp"
"strings"
"github.com/pkg/errors"
)
// scpSyntax matches the SCP-like addresses used by the ssh protocol (eg, [user@]host.xz:path/to/repo.git/).
// See: https://golang.org/src/cmd/go/internal/get/vcs.go
var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
func ParseURL(rawURL string) (url *urlpkg.URL, err error) {
// If rawURL matches the SCP-like syntax, convert it into a standard ssh Path.
// eg, git@github.com:user/repo => ssh://git@github.com/user/repo
if m := scpSyntax.FindStringSubmatch(rawURL); m != nil {
url = &urlpkg.URL{
Scheme: "ssh",
User: urlpkg.User(m[1]),
Host: m[2],
Path: m[3],
}
} else {
url, err = urlpkg.Parse(rawURL)
if err != nil {
return nil, errors.Wrap(err, "Failed parsing Path")
}
}
if url.Host == "" && url.Path == "" {
return nil, errors.New("Parsed Path is empty")
}
if url.Scheme == "git+ssh" {
url.Scheme = "ssh"
}
// Default to "git" user when using ssh and no user is provided
if url.Scheme == "ssh" && url.User == nil {
url.User = urlpkg.User("git")
}
// Default to https
if url.Scheme == "" {
url.Scheme = "https"
}
// TODO: Default to github host
return url, nil
}
func URLToPath(url *urlpkg.URL) (repoPath string) {
// Remove port numbers from host
repoHost := strings.Split(url.Host, ":")[0]
// Remove trailing ".git" from repo name
repoPath = path.Join(repoHost, url.Path)
repoPath = strings.TrimSuffix(repoPath, ".git")
// Remove tilde (~) char from username
repoPath = strings.ReplaceAll(repoPath, "~", "")
return repoPath
}

81
new/url_test.go Normal file
View File

@@ -0,0 +1,81 @@
package new
import (
"testing"
)
// Following URLs are considered valid according to https://git-scm.com/docs/git-clone#_git_urls:
// ssh://[user@]host.xz[:port]/path/to/repo.git
// ssh://[user@]host.xz[:port]/~[user]/path/to/repo.git/
// [user@]host.xz:path/to/repo.git/
// [user@]host.xz:/~[user]/path/to/repo.git/
// git://host.xz[:port]/path/to/repo.git/
// git://host.xz[:port]/~[user]/path/to/repo.git/
// http[s]://host.xz[:port]/path/to/repo.git/
// ftp[s]://host.xz[:port]/path/to/repo.git/
// /path/to/repo.git/
// file:///path/to/repo.git/
func TestURLParse(t *testing.T) {
tests := []struct {
in string
want string
}{
{"ssh://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"ssh://user@github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"ssh://user@github.com:1234/grdl/git-get.git", "github.com/grdl/git-get"},
{"ssh://user@github.com/~user/grdl/git-get.git", "github.com/user/grdl/git-get"},
{"git+ssh://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"git@github.com:grdl/git-get.git", "github.com/grdl/git-get"},
{"git@github.com:/~user/grdl/git-get.git", "github.com/user/grdl/git-get"},
{"git://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"git://github.com/~user/grdl/git-get.git", "github.com/user/grdl/git-get"},
{"https://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"http://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"https://github.com/grdl/git-get", "github.com/grdl/git-get"},
{"https://github.com/git-get.git", "github.com/git-get"},
{"https://github.com/git-get", "github.com/git-get"},
{"https://github.com/grdl/sub/path/git-get.git", "github.com/grdl/sub/path/git-get"},
{"https://github.com:1234/grdl/git-get.git", "github.com/grdl/git-get"},
{"https://github.com/grdl/git-get.git/", "github.com/grdl/git-get"},
{"https://github.com/grdl/git-get/", "github.com/grdl/git-get"},
{"https://github.com/grdl/git-get/////", "github.com/grdl/git-get"},
{"https://github.com/grdl/git-get.git/////", "github.com/grdl/git-get"},
{"ftp://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"ftps://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"rsync://github.com/grdl/git-get.git", "github.com/grdl/git-get"},
{"local/grdl/git-get/", "local/grdl/git-get"},
{"file://local/grdl/git-get", "local/grdl/git-get"},
}
for _, test := range tests {
url, err := ParseURL(test.in)
if err != nil {
t.Errorf("Error parsing Path: %+v", err)
}
got := URLToPath(url)
if got != test.want {
t.Errorf("Wrong result of parsing Path: %s, got: %s; want: %s", test.in, got, test.want)
}
}
}
func TestInvalidURLParse(t *testing.T) {
invalidURLs := []string{
"",
//TODO: This Path is technically a correct scp-like syntax. Not sure how to handle it
"github.com:grdl/git-git.get.git",
//TODO: Is this a valid git Path?
//"git@github.com:1234:grdl/git-get.git",
}
for _, in := range invalidURLs {
got, err := ParseURL(in)
if err == nil {
t.Errorf("Wrong result of parsing invalid Path: %s, got: %s, want: error", in, got)
}
}
}