6
0
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:
Grzegorz Dlugoszewski
2020-06-18 14:16:59 +02:00
parent da8f0931d0
commit 8511cd6c97
12 changed files with 65 additions and 65 deletions

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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