mirror of
https://github.com/grdl/git-get.git
synced 2026-02-04 19:09:45 +00:00
Rename git package to repo package
This commit is contained in:
145
pkg/git/repo.go
145
pkg/git/repo.go
@@ -1,145 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git-get/pkg/cfg"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
go_git_ssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||
)
|
||||
|
||||
type Repo struct {
|
||||
*git.Repository
|
||||
Path string
|
||||
Status *RepoStatus
|
||||
}
|
||||
|
||||
// CloneOpts specify details about repository to clone.
|
||||
type CloneOpts struct {
|
||||
URL *url.URL
|
||||
Path string // TODO: should Path be a part of clone opts?
|
||||
Branch string
|
||||
Quiet bool
|
||||
IgnoreExisting bool // TODO: implement!
|
||||
}
|
||||
|
||||
func CloneRepo(opts *CloneOpts) (*Repo, error) {
|
||||
var progress io.Writer
|
||||
if !opts.Quiet {
|
||||
progress = os.Stdout
|
||||
fmt.Printf("Cloning into '%s'...\n", opts.Path)
|
||||
}
|
||||
|
||||
// TODO: can this be cleaner?
|
||||
var auth transport.AuthMethod
|
||||
var err error
|
||||
if opts.URL.Scheme == "ssh" {
|
||||
if auth, err = sshKeyAuth(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// If branch name is actually a tag (ie. is prefixed with refs/tags) - check out that tag.
|
||||
// Otherwise, assume it's a branch name and check it out.
|
||||
refName := plumbing.ReferenceName(opts.Branch)
|
||||
if !refName.IsTag() {
|
||||
refName = plumbing.NewBranchReferenceName(opts.Branch)
|
||||
}
|
||||
|
||||
gitOpts := &git.CloneOptions{
|
||||
URL: opts.URL.String(),
|
||||
Auth: auth,
|
||||
RemoteName: git.DefaultRemoteName,
|
||||
ReferenceName: refName,
|
||||
SingleBranch: false,
|
||||
NoCheckout: false,
|
||||
Depth: 0,
|
||||
RecurseSubmodules: git.NoRecurseSubmodules,
|
||||
Progress: progress,
|
||||
Tags: git.AllTags,
|
||||
}
|
||||
|
||||
repo, err := git.PlainClone(opts.Path, false, gitOpts)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed cloning repo")
|
||||
}
|
||||
|
||||
return NewRepo(repo, opts.Path), nil
|
||||
}
|
||||
|
||||
func OpenRepo(repoPath string) (*Repo, error) {
|
||||
repo, err := git.PlainOpen(repoPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed opening repo")
|
||||
}
|
||||
|
||||
return NewRepo(repo, repoPath), nil
|
||||
}
|
||||
|
||||
func NewRepo(repo *git.Repository, repoPath string) *Repo {
|
||||
return &Repo{
|
||||
Repository: repo,
|
||||
Path: repoPath,
|
||||
Status: &RepoStatus{},
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch performs a git fetch on all remotes
|
||||
func (r *Repo) Fetch() error {
|
||||
remotes, err := r.Remotes()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed getting remotes")
|
||||
}
|
||||
|
||||
for _, remote := range remotes {
|
||||
err = remote.Fetch(&git.FetchOptions{})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Failed fetching remote %s", remote.Config().Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sshKeyAuth() (transport.AuthMethod, error) {
|
||||
privateKey := viper.GetString(cfg.KeyPrivateKey)
|
||||
sshKey, err := ioutil.ReadFile(privateKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to open ssh private key %s", privateKey)
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey([]byte(sshKey))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to parse ssh private key %s", privateKey)
|
||||
}
|
||||
|
||||
// TODO: can it ba a different user
|
||||
auth := &go_git_ssh.PublicKeys{User: "git", Signer: signer}
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func (r *Repo) CurrentBranchStatus() *BranchStatus {
|
||||
if r.Status.CurrentBranch == StatusDetached || r.Status.CurrentBranch == StatusUnknown {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, b := range r.Status.Branches {
|
||||
if b.Name == r.Status.CurrentBranch {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"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"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
testUser = "Test User"
|
||||
testEmail = "testuser@example.com"
|
||||
)
|
||||
|
||||
func newRepoEmpty(t *testing.T) *Repo {
|
||||
dir := newTempDir(t)
|
||||
|
||||
repo, err := git.PlainInit(dir, false)
|
||||
checkFatal(t, err)
|
||||
|
||||
return NewRepo(repo, dir)
|
||||
}
|
||||
|
||||
func newRepoWithUntracked(t *testing.T) *Repo {
|
||||
r := newRepoEmpty(t)
|
||||
r.writeFile(t, "README", "I'm a README file")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithStaged(t *testing.T) *Repo {
|
||||
r := newRepoEmpty(t)
|
||||
r.writeFile(t, "README", "I'm a README file")
|
||||
r.addFile(t, "README")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithCommit(t *testing.T) *Repo {
|
||||
r := newRepoEmpty(t)
|
||||
r.writeFile(t, "README", "I'm a README file")
|
||||
r.addFile(t, "README")
|
||||
r.newCommit(t, "Initial commit")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithModified(t *testing.T) *Repo {
|
||||
r := newRepoEmpty(t)
|
||||
r.writeFile(t, "README", "I'm a README file")
|
||||
r.addFile(t, "README")
|
||||
r.newCommit(t, "Initial commit")
|
||||
r.writeFile(t, "README", "I'm modified")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithIgnored(t *testing.T) *Repo {
|
||||
r := newRepoEmpty(t)
|
||||
r.writeFile(t, ".gitignore", "ignoreme")
|
||||
r.addFile(t, ".gitignore")
|
||||
r.newCommit(t, "Initial commit")
|
||||
r.writeFile(t, "ignoreme", "I'm being ignored")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithLocalBranch(t *testing.T) *Repo {
|
||||
r := newRepoWithCommit(t)
|
||||
r.newBranch(t, "local")
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithClonedBranch(t *testing.T) *Repo {
|
||||
origin := newRepoWithCommit(t)
|
||||
|
||||
r := origin.clone(t, "master")
|
||||
r.newBranch(t, "local")
|
||||
r.checkoutBranch(t, "local")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithDetachedHead(t *testing.T) *Repo {
|
||||
r := newRepoWithCommit(t)
|
||||
|
||||
r.writeFile(t, "new", "I'm a new file")
|
||||
r.addFile(t, "new")
|
||||
hash := r.newCommit(t, "new commit")
|
||||
|
||||
r.checkoutHash(t, hash)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithBranchAhead(t *testing.T) *Repo {
|
||||
origin := newRepoWithCommit(t)
|
||||
|
||||
r := origin.clone(t, "master")
|
||||
r.writeFile(t, "new", "I'm a new file")
|
||||
r.addFile(t, "new")
|
||||
r.newCommit(t, "new commit")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithBranchBehind(t *testing.T) *Repo {
|
||||
origin := newRepoWithCommit(t)
|
||||
|
||||
r := origin.clone(t, "master")
|
||||
|
||||
origin.writeFile(t, "origin.new", "I'm a new file on origin")
|
||||
origin.addFile(t, "origin.new")
|
||||
origin.newCommit(t, "new origin commit")
|
||||
|
||||
r.fetch(t)
|
||||
return r
|
||||
}
|
||||
|
||||
// generate repo with 2 commits ahead and 3 behind the origin
|
||||
func newRepoWithBranchAheadAndBehind(t *testing.T) *Repo {
|
||||
origin := newRepoWithCommit(t)
|
||||
|
||||
r := origin.clone(t, "master")
|
||||
r.writeFile(t, "local.new", "local 1")
|
||||
r.addFile(t, "local.new")
|
||||
r.newCommit(t, "1st local commit")
|
||||
|
||||
r.writeFile(t, "local.new", "local 2")
|
||||
r.addFile(t, "local.new")
|
||||
r.newCommit(t, "2nd local commit")
|
||||
|
||||
origin.writeFile(t, "origin.new", "origin 1")
|
||||
origin.addFile(t, "origin.new")
|
||||
origin.newCommit(t, "1st origin commit")
|
||||
|
||||
origin.writeFile(t, "origin.new", "origin 2")
|
||||
origin.addFile(t, "origin.new")
|
||||
origin.newCommit(t, "2nd origin commit")
|
||||
|
||||
origin.writeFile(t, "origin.new", "origin 3")
|
||||
origin.addFile(t, "origin.new")
|
||||
origin.newCommit(t, "3rd origin commit")
|
||||
|
||||
r.fetch(t)
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithCheckedOutBranch(t *testing.T) *Repo {
|
||||
origin := newRepoWithCommit(t)
|
||||
origin.newBranch(t, "feature/branch1")
|
||||
|
||||
r := origin.clone(t, "feature/branch1")
|
||||
return r
|
||||
}
|
||||
|
||||
func newRepoWithCheckedOutTag(t *testing.T) *Repo {
|
||||
origin := newRepoWithCommit(t)
|
||||
origin.newTag(t, "v1.0.0")
|
||||
|
||||
r := origin.clone(t, "refs/tags/v1.0.0")
|
||||
return r
|
||||
}
|
||||
|
||||
func newTempDir(t *testing.T) string {
|
||||
dir, err := ioutil.TempDir("", "git-get-repo-")
|
||||
checkFatal(t, errors.Wrap(err, "Failed creating test repo directory"))
|
||||
|
||||
// Automatically remove repo when test is over
|
||||
t.Cleanup(func() {
|
||||
err := os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
t.Errorf("failed cleaning up repo")
|
||||
}
|
||||
})
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
func (r *Repo) writeFile(t *testing.T, name string, content string) {
|
||||
wt, err := r.Worktree()
|
||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||
|
||||
file, err := wt.Filesystem.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
checkFatal(t, errors.Wrap(err, "Failed opening a file"))
|
||||
|
||||
_, err = file.Write([]byte(content))
|
||||
checkFatal(t, errors.Wrap(err, "Failed writing a file"))
|
||||
}
|
||||
|
||||
func (r *Repo) addFile(t *testing.T, name string) {
|
||||
wt, err := r.Worktree()
|
||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||
|
||||
_, err = wt.Add(name)
|
||||
checkFatal(t, errors.Wrap(err, "Failed adding file to index"))
|
||||
}
|
||||
|
||||
func (r *Repo) newCommit(t *testing.T, msg string) plumbing.Hash {
|
||||
wt, err := r.Worktree()
|
||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||
|
||||
opts := &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: testUser,
|
||||
Email: testEmail,
|
||||
When: time.Date(2000, 01, 01, 16, 00, 00, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
|
||||
hash, err := wt.Commit(msg, opts)
|
||||
checkFatal(t, errors.Wrap(err, "Failed creating commit"))
|
||||
return hash
|
||||
}
|
||||
|
||||
func (r *Repo) newBranch(t *testing.T, name string) {
|
||||
head, err := r.Head()
|
||||
checkFatal(t, err)
|
||||
|
||||
ref := plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), head.Hash())
|
||||
|
||||
err = r.Storer.SetReference(ref)
|
||||
checkFatal(t, err)
|
||||
}
|
||||
|
||||
func (r *Repo) newTag(t *testing.T, name string) {
|
||||
head, err := r.Head()
|
||||
checkFatal(t, err)
|
||||
|
||||
ref := plumbing.NewHashReference(plumbing.NewTagReferenceName(name), head.Hash())
|
||||
|
||||
err = r.Storer.SetReference(ref)
|
||||
checkFatal(t, err)
|
||||
}
|
||||
|
||||
func (r *Repo) clone(t *testing.T, branch string) *Repo {
|
||||
dir := newTempDir(t)
|
||||
repoURL, err := url.Parse("file://" + r.Path)
|
||||
checkFatal(t, err)
|
||||
|
||||
cloneOpts := &CloneOpts{
|
||||
URL: repoURL,
|
||||
Path: dir,
|
||||
Branch: branch,
|
||||
Quiet: true,
|
||||
}
|
||||
|
||||
repo, err := CloneRepo(cloneOpts)
|
||||
checkFatal(t, err)
|
||||
|
||||
return repo
|
||||
}
|
||||
|
||||
func (r *Repo) fetch(t *testing.T) {
|
||||
err := r.Fetch()
|
||||
checkFatal(t, err)
|
||||
}
|
||||
|
||||
func (r *Repo) checkoutBranch(t *testing.T, name string) {
|
||||
wt, err := r.Worktree()
|
||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||
|
||||
opts := &git.CheckoutOptions{
|
||||
Branch: plumbing.NewBranchReferenceName(name),
|
||||
}
|
||||
err = wt.Checkout(opts)
|
||||
checkFatal(t, errors.Wrap(err, "Failed checking out branch"))
|
||||
}
|
||||
|
||||
func (r *Repo) checkoutHash(t *testing.T, hash plumbing.Hash) {
|
||||
wt, err := r.Worktree()
|
||||
checkFatal(t, errors.Wrap(err, "Failed getting worktree"))
|
||||
|
||||
opts := &git.CheckoutOptions{
|
||||
Hash: hash,
|
||||
}
|
||||
err = wt.Checkout(opts)
|
||||
checkFatal(t, errors.Wrap(err, "Failed checking out hash"))
|
||||
}
|
||||
|
||||
func checkFatal(t *testing.T, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("%+v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"git-get/pkg/cfg"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/revlist"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
StatusUnknown = "unknown"
|
||||
StatusDetached = "detached HEAD"
|
||||
StatusNoUpstream = "no upstream"
|
||||
StatusAhead = "ahead"
|
||||
StatusBehind = "behind"
|
||||
StatusOk = "ok"
|
||||
StatusUncommitted = "uncommitted"
|
||||
StatusUntracked = "untracked"
|
||||
)
|
||||
|
||||
type RepoStatus struct {
|
||||
HasUntrackedFiles bool
|
||||
HasUncommittedChanges bool
|
||||
CurrentBranch string
|
||||
Branches []*BranchStatus
|
||||
}
|
||||
|
||||
type BranchStatus struct {
|
||||
Name string
|
||||
Upstream string
|
||||
Ahead int
|
||||
Behind int
|
||||
}
|
||||
|
||||
func (r *Repo) LoadStatus() error {
|
||||
// Fetch from remotes if executed with --fetch flag. Ignore the "already up-to-date" errors.
|
||||
if viper.GetBool(cfg.KeyFetch) {
|
||||
err := r.Fetch()
|
||||
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||
return errors.Wrap(err, "Failed fetching from remotes")
|
||||
}
|
||||
}
|
||||
|
||||
wt, err := r.Worktree()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed getting worktree")
|
||||
}
|
||||
|
||||
// worktree.Status doesn't load gitignore patterns that are defined outside of .gitignore file using excludesfile.
|
||||
// We need to load them explicitly here
|
||||
// TODO: variables are not expanded so if excludesfile is declared like "~/gitignore_global" or "$HOME/gitignore_global", this will fail to open it
|
||||
globalPatterns, err := gitignore.LoadGlobalPatterns(osfs.New(""))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed loading global gitignore patterns")
|
||||
}
|
||||
wt.Excludes = append(wt.Excludes, globalPatterns...)
|
||||
|
||||
systemPatterns, err := gitignore.LoadSystemPatterns(osfs.New(""))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed loading system gitignore patterns")
|
||||
}
|
||||
wt.Excludes = append(wt.Excludes, systemPatterns...)
|
||||
|
||||
status, err := wt.Status()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed getting worktree status")
|
||||
}
|
||||
|
||||
r.Status.HasUncommittedChanges = hasUncommitted(status)
|
||||
r.Status.HasUntrackedFiles = hasUntracked(status)
|
||||
r.Status.CurrentBranch = currentBranch(r)
|
||||
|
||||
err = r.loadBranchesStatus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasUntracked returns true if there are any untracked files in the worktree
|
||||
func hasUntracked(status git.Status) bool {
|
||||
for _, fs := range status {
|
||||
if fs.Worktree == git.Untracked || fs.Staging == git.Untracked {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// hasUncommitted returns true if there are any uncommitted (but tracked) files in the worktree
|
||||
func hasUncommitted(status git.Status) bool {
|
||||
// If repo is clean it means every file in worktree and staging has 'Unmodified' state
|
||||
if status.IsClean() {
|
||||
return false
|
||||
}
|
||||
|
||||
// If repo is not clean, check if any file has state different than 'Untracked' - it means they are tracked and have uncommitted modifications
|
||||
for _, fs := range status {
|
||||
if fs.Worktree != git.Untracked || fs.Staging != git.Untracked {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func currentBranch(r *Repo) string {
|
||||
head, err := r.Head()
|
||||
if err != nil {
|
||||
return StatusUnknown
|
||||
}
|
||||
|
||||
if head.Name().Short() == plumbing.HEAD.String() {
|
||||
return StatusDetached
|
||||
}
|
||||
|
||||
return head.Name().Short()
|
||||
}
|
||||
|
||||
func (r *Repo) loadBranchesStatus() error {
|
||||
iter, err := r.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 = append(r.Status.Branches, bs)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed iterating over branches")
|
||||
}
|
||||
|
||||
// Sort branches by name (but with "master" always at the top). It's useful to have them sorted for printing and testing.
|
||||
sort.Slice(r.Status.Branches, func(i, j int) bool {
|
||||
if r.Status.Branches[i].Name == "master" {
|
||||
return true
|
||||
}
|
||||
|
||||
return strings.Compare(r.Status.Branches[i].Name, r.Status.Branches[j].Name) < 0
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repo) newBranchStatus(branch string) (*BranchStatus, error) {
|
||||
bs := &BranchStatus{
|
||||
Name: branch,
|
||||
}
|
||||
|
||||
upstream, err := r.upstream(branch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if upstream == "" {
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
ahead, behind, err := r.aheadBehind(branch, upstream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bs.Upstream = upstream
|
||||
bs.Ahead = ahead
|
||||
bs.Behind = behind
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// upstream finds if a given branch tracks an upstream.
|
||||
// Returns found upstream branch name (eg, origin/master) or empty string 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 upstream 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) (string, error) {
|
||||
cfg, err := r.Config()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "Failed getting repo config")
|
||||
}
|
||||
|
||||
// Check if our branch exists in "branch" config sections. If not, it doesn't have an upstream configured.
|
||||
bcfg := cfg.Branches[branch]
|
||||
if bcfg == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
remote := bcfg.Remote
|
||||
if remote == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
merge := bcfg.Merge.Short()
|
||||
if merge == "" {
|
||||
return "", nil
|
||||
}
|
||||
return remote + "/" + merge, nil
|
||||
}
|
||||
|
||||
func (r *Repo) aheadBehind(localBranch string, upstreamBranch string) (ahead int, behind int, err error) {
|
||||
localHash, err := r.ResolveRevision(plumbing.Revision(localBranch))
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrapf(err, "Failed resolving revision %s", localBranch)
|
||||
}
|
||||
|
||||
upstreamHash, err := r.ResolveRevision(plumbing.Revision(upstreamBranch))
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrapf(err, "Failed resolving revision %s", upstreamBranch)
|
||||
}
|
||||
|
||||
behind, err = r.revlistCount(*localHash, *upstreamHash)
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrapf(err, "Failed counting commits behind %s", upstreamBranch)
|
||||
}
|
||||
|
||||
ahead, err = r.revlistCount(*upstreamHash, *localHash)
|
||||
if err != nil {
|
||||
return 0, 0, errors.Wrapf(err, "Failed counting commits ahead of %s", upstreamBranch)
|
||||
}
|
||||
|
||||
return ahead, behind, nil
|
||||
}
|
||||
|
||||
// revlistCount counts the number of commits between two hashes.
|
||||
// https://github.com/src-d/go-git/issues/757#issuecomment-452697701
|
||||
// TODO: See if this can be optimized. Running the loop twice feels wrong.
|
||||
func (r *Repo) revlistCount(hash1, hash2 plumbing.Hash) (int, error) {
|
||||
ref1hist, err := revlist.Objects(r.Storer, []plumbing.Hash{hash1}, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ref2hist, err := revlist.Objects(r.Storer, []plumbing.Hash{hash2}, ref1hist)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, h := range ref2hist {
|
||||
if _, err = r.CommitObject(h); err == nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStatus(t *testing.T) {
|
||||
var tests = []struct {
|
||||
makeTestRepo func(*testing.T) *Repo
|
||||
want *RepoStatus
|
||||
}{
|
||||
{newRepoEmpty, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: StatusUnknown,
|
||||
Branches: nil,
|
||||
}},
|
||||
{newRepoWithUntracked, &RepoStatus{
|
||||
HasUntrackedFiles: true,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: StatusUnknown,
|
||||
Branches: nil,
|
||||
}},
|
||||
{newRepoWithStaged, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: true,
|
||||
CurrentBranch: StatusUnknown,
|
||||
Branches: nil,
|
||||
}},
|
||||
{newRepoWithCommit, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "master",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithModified, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: true,
|
||||
CurrentBranch: "master",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithIgnored, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "master",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithLocalBranch, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "master",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
}, {
|
||||
Name: "local",
|
||||
Upstream: "",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithClonedBranch, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "local",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "origin/master",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
}, {
|
||||
Name: "local",
|
||||
Upstream: "",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithDetachedHead, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: StatusDetached,
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithBranchAhead, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "master",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "origin/master",
|
||||
Behind: 0,
|
||||
Ahead: 1,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithBranchBehind, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "master",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "origin/master",
|
||||
Behind: 1,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithBranchAheadAndBehind, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "master",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "master",
|
||||
Upstream: "origin/master",
|
||||
Behind: 3,
|
||||
Ahead: 2,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithCheckedOutBranch, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
CurrentBranch: "feature/branch1",
|
||||
Branches: []*BranchStatus{
|
||||
{
|
||||
Name: "feature/branch1",
|
||||
Upstream: "origin/feature/branch1",
|
||||
Behind: 0,
|
||||
Ahead: 0,
|
||||
},
|
||||
},
|
||||
}},
|
||||
{newRepoWithCheckedOutTag, &RepoStatus{
|
||||
HasUntrackedFiles: false,
|
||||
HasUncommittedChanges: false,
|
||||
// TODO: is this correct? Can we show tag name instead of "detached HEAD"?
|
||||
CurrentBranch: StatusDetached,
|
||||
Branches: nil,
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
repo := test.makeTestRepo(t)
|
||||
|
||||
err := repo.LoadStatus()
|
||||
checkFatal(t, err)
|
||||
|
||||
if !reflect.DeepEqual(repo.Status, test.want) {
|
||||
t.Errorf("Failed test case %d, got: %+v; want: %+v", i, repo.Status, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test branch status when tracking a local branch
|
||||
// TODO: test head pointing to a tag
|
||||
// TODO: newRepoWithGlobalGitignore
|
||||
// TODO: newRepoWithGlobalGitignoreSymlink
|
||||
Reference in New Issue
Block a user