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:
1
go.mod
1
go.mod
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
//
|
//
|
||||||
|
|||||||
25
new/repo.go
25
new/repo.go
@@ -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),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
44
new/status_test.go
Normal 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
68
new/url.go
Normal 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
81
new/url_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user