diff options
author | Michael Muré <batolettre@gmail.com> | 2020-09-29 20:51:15 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-09-29 20:51:15 +0200 |
commit | 1204b66e0cc958c2ca3b328d25cbec347356a046 (patch) | |
tree | 852ba5a688eea6872b0885d23dc91342d09b468d /repository/gogit.go | |
parent | 9f3a56b1f34a8b4a7a75357986e967afc4b96611 (diff) | |
parent | 4055495c8ba983033459507f3032ca93c6ec006a (diff) | |
download | git-bug-1204b66e0cc958c2ca3b328d25cbec347356a046.tar.gz |
Merge pull request #412 from MichaelMure/gogit-repo
repository: go-git backed Repo
Diffstat (limited to 'repository/gogit.go')
-rw-r--r-- | repository/gogit.go | 615 |
1 files changed, 615 insertions, 0 deletions
diff --git a/repository/gogit.go b/repository/gogit.go new file mode 100644 index 00000000..09f714ea --- /dev/null +++ b/repository/gogit.go @@ -0,0 +1,615 @@ +package repository + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + stdpath "path" + "path/filepath" + "strings" + "sync" + "time" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/object" + + "github.com/MichaelMure/git-bug/util/lamport" +) + +var _ ClockedRepo = &GoGitRepo{} + +type GoGitRepo struct { + r *gogit.Repository + path string + + clocksMutex sync.Mutex + clocks map[string]lamport.Clock + + keyring Keyring +} + +func NewGoGitRepo(path string, clockLoaders []ClockLoader) (*GoGitRepo, error) { + path, err := detectGitPath(path) + if err != nil { + return nil, err + } + + r, err := gogit.PlainOpen(path) + if err != nil { + return nil, err + } + + k, err := defaultKeyring() + if err != nil { + return nil, err + } + + repo := &GoGitRepo{ + r: r, + path: path, + clocks: make(map[string]lamport.Clock), + keyring: k, + } + + for _, loader := range clockLoaders { + allExist := true + for _, name := range loader.Clocks { + if _, err := repo.getClock(name); err != nil { + allExist = false + } + } + + if !allExist { + err = loader.Witnesser(repo) + if err != nil { + return nil, err + } + } + } + + return repo, nil +} + +func detectGitPath(path string) (string, error) { + // normalize the path + path, err := filepath.Abs(path) + if err != nil { + return "", err + } + + for { + fi, err := os.Stat(stdpath.Join(path, ".git")) + if err == nil { + if !fi.IsDir() { + return "", fmt.Errorf(".git exist but is not a directory") + } + return stdpath.Join(path, ".git"), nil + } + if !os.IsNotExist(err) { + // unknown error + return "", err + } + + // detect bare repo + ok, err := isGitDir(path) + if err != nil { + return "", err + } + if ok { + return path, nil + } + + if parent := filepath.Dir(path); parent == path { + return "", fmt.Errorf(".git not found") + } else { + path = parent + } + } +} + +func isGitDir(path string) (bool, error) { + markers := []string{"HEAD", "objects", "refs"} + + for _, marker := range markers { + _, err := os.Stat(stdpath.Join(path, marker)) + if err == nil { + continue + } + if !os.IsNotExist(err) { + // unknown error + return false, err + } else { + return false, nil + } + } + + return true, nil +} + +// InitGoGitRepo create a new empty git repo at the given path +func InitGoGitRepo(path string) (*GoGitRepo, error) { + r, err := gogit.PlainInit(path, false) + if err != nil { + return nil, err + } + + return &GoGitRepo{ + r: r, + path: path + "/.git", + clocks: make(map[string]lamport.Clock), + }, nil +} + +// InitBareGoGitRepo create a new --bare empty git repo at the given path +func InitBareGoGitRepo(path string) (*GoGitRepo, error) { + r, err := gogit.PlainInit(path, true) + if err != nil { + return nil, err + } + + return &GoGitRepo{ + r: r, + path: path, + clocks: make(map[string]lamport.Clock), + }, nil +} + +// LocalConfig give access to the repository scoped configuration +func (repo *GoGitRepo) LocalConfig() Config { + return newGoGitLocalConfig(repo.r) +} + +// GlobalConfig give access to the global scoped configuration +func (repo *GoGitRepo) GlobalConfig() Config { + // TODO: replace that with go-git native implementation once it's supported + // see: https://github.com/go-git/go-git + // see: https://github.com/src-d/go-git/issues/760 + return newGoGitGlobalConfig(repo.r) +} + +// AnyConfig give access to a merged local/global configuration +func (repo *GoGitRepo) AnyConfig() ConfigRead { + return mergeConfig(repo.LocalConfig(), repo.GlobalConfig()) +} + +// Keyring give access to a user-wide storage for secrets +func (repo *GoGitRepo) Keyring() Keyring { + return repo.keyring +} + +// GetPath returns the path to the repo. +func (repo *GoGitRepo) GetPath() string { + return repo.path +} + +// GetUserName returns the name the the user has used to configure git +func (repo *GoGitRepo) GetUserName() (string, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + return cfg.User.Name, nil +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (repo *GoGitRepo) GetUserEmail() (string, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + return cfg.User.Email, nil +} + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (repo *GoGitRepo) GetCoreEditor() (string, error) { + // See https://git-scm.com/docs/git-var + // The order of preference is the $GIT_EDITOR environment variable, then core.editor configuration, then $VISUAL, then $EDITOR, and then the default chosen at compile time, which is usually vi. + + if val, ok := os.LookupEnv("GIT_EDITOR"); ok { + return val, nil + } + + val, err := repo.AnyConfig().ReadString("core.editor") + if err == nil && val != "" { + return val, nil + } + if err != nil && err != ErrNoConfigEntry { + return "", err + } + + if val, ok := os.LookupEnv("VISUAL"); ok { + return val, nil + } + + if val, ok := os.LookupEnv("EDITOR"); ok { + return val, nil + } + + return "vi", nil +} + +// GetRemotes returns the configured remotes repositories. +func (repo *GoGitRepo) GetRemotes() (map[string]string, error) { + cfg, err := repo.r.Config() + if err != nil { + return nil, err + } + + result := make(map[string]string, len(cfg.Remotes)) + for name, remote := range cfg.Remotes { + if len(remote.URLs) > 0 { + result[name] = remote.URLs[0] + } + } + + return result, nil +} + +// FetchRefs fetch git refs from a remote +func (repo *GoGitRepo) FetchRefs(remote string, refSpec string) (string, error) { + buf := bytes.NewBuffer(nil) + + err := repo.r.Fetch(&gogit.FetchOptions{ + RemoteName: remote, + RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, + Progress: buf, + }) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// PushRefs push git refs to a remote +func (repo *GoGitRepo) PushRefs(remote string, refSpec string) (string, error) { + buf := bytes.NewBuffer(nil) + + err := repo.r.Push(&gogit.PushOptions{ + RemoteName: remote, + RefSpecs: []config.RefSpec{config.RefSpec(refSpec)}, + Progress: buf, + }) + if err != nil { + return "", err + } + + return buf.String(), nil +} + +// StoreData will store arbitrary data and return the corresponding hash +func (repo *GoGitRepo) StoreData(data []byte) (Hash, error) { + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.BlobObject) + + w, err := obj.Writer() + if err != nil { + return "", err + } + + _, err = w.Write(data) + if err != nil { + return "", err + } + + h, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(h.String()), nil +} + +// ReadData will attempt to read arbitrary data from the given hash +func (repo *GoGitRepo) ReadData(hash Hash) ([]byte, error) { + obj, err := repo.r.BlobObject(plumbing.NewHash(hash.String())) + if err != nil { + return nil, err + } + + r, err := obj.Reader() + if err != nil { + return nil, err + } + + return ioutil.ReadAll(r) +} + +// StoreTree will store a mapping key-->Hash as a Git tree +func (repo *GoGitRepo) StoreTree(mapping []TreeEntry) (Hash, error) { + var tree object.Tree + + for _, entry := range mapping { + mode := filemode.Regular + if entry.ObjectType == Tree { + mode = filemode.Dir + } + + tree.Entries = append(tree.Entries, object.TreeEntry{ + Name: entry.Name, + Mode: mode, + Hash: plumbing.NewHash(entry.Hash.String()), + }) + } + + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.TreeObject) + err := tree.Encode(obj) + if err != nil { + return "", err + } + + hash, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(hash.String()), nil +} + +// ReadTree will return the list of entries in a Git tree +func (repo *GoGitRepo) ReadTree(hash Hash) ([]TreeEntry, error) { + h := plumbing.NewHash(hash.String()) + + // the given hash could be a tree or a commit + obj, err := repo.r.Storer.EncodedObject(plumbing.AnyObject, h) + if err != nil { + return nil, err + } + + var tree *object.Tree + switch obj.Type() { + case plumbing.TreeObject: + tree, err = object.DecodeTree(repo.r.Storer, obj) + case plumbing.CommitObject: + var commit *object.Commit + commit, err = object.DecodeCommit(repo.r.Storer, obj) + if err != nil { + return nil, err + } + tree, err = commit.Tree() + default: + return nil, fmt.Errorf("given hash is not a tree") + } + if err != nil { + return nil, err + } + + treeEntries := make([]TreeEntry, len(tree.Entries)) + for i, entry := range tree.Entries { + objType := Blob + if entry.Mode == filemode.Dir { + objType = Tree + } + + treeEntries[i] = TreeEntry{ + ObjectType: objType, + Hash: Hash(entry.Hash.String()), + Name: entry.Name, + } + } + + return treeEntries, nil +} + +// StoreCommit will store a Git commit with the given Git tree +func (repo *GoGitRepo) StoreCommit(treeHash Hash) (Hash, error) { + return repo.StoreCommitWithParent(treeHash, "") +} + +// StoreCommit will store a Git commit with the given Git tree +func (repo *GoGitRepo) StoreCommitWithParent(treeHash Hash, parent Hash) (Hash, error) { + cfg, err := repo.r.Config() + if err != nil { + return "", err + } + + commit := object.Commit{ + Author: object.Signature{ + cfg.Author.Name, + cfg.Author.Email, + time.Now(), + }, + Committer: object.Signature{ + cfg.Committer.Name, + cfg.Committer.Email, + time.Now(), + }, + Message: "", + TreeHash: plumbing.NewHash(treeHash.String()), + } + + if parent != "" { + commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(parent.String())} + } + + obj := repo.r.Storer.NewEncodedObject() + obj.SetType(plumbing.CommitObject) + err = commit.Encode(obj) + if err != nil { + return "", err + } + + hash, err := repo.r.Storer.SetEncodedObject(obj) + if err != nil { + return "", err + } + + return Hash(hash.String()), nil +} + +// GetTreeHash return the git tree hash referenced in a commit +func (repo *GoGitRepo) GetTreeHash(commit Hash) (Hash, error) { + obj, err := repo.r.CommitObject(plumbing.NewHash(commit.String())) + if err != nil { + return "", err + } + + return Hash(obj.TreeHash.String()), nil +} + +// FindCommonAncestor will return the last common ancestor of two chain of commit +func (repo *GoGitRepo) FindCommonAncestor(commit1 Hash, commit2 Hash) (Hash, error) { + obj1, err := repo.r.CommitObject(plumbing.NewHash(commit1.String())) + if err != nil { + return "", err + } + obj2, err := repo.r.CommitObject(plumbing.NewHash(commit2.String())) + if err != nil { + return "", err + } + + commits, err := obj1.MergeBase(obj2) + if err != nil { + return "", err + } + + return Hash(commits[0].Hash.String()), nil +} + +// UpdateRef will create or update a Git reference +func (repo *GoGitRepo) UpdateRef(ref string, hash Hash) error { + return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(ref), plumbing.NewHash(hash.String()))) +} + +// RemoveRef will remove a Git reference +func (repo *GoGitRepo) RemoveRef(ref string) error { + return repo.r.Storer.RemoveReference(plumbing.ReferenceName(ref)) +} + +// ListRefs will return a list of Git ref matching the given refspec +func (repo *GoGitRepo) ListRefs(refPrefix string) ([]string, error) { + refIter, err := repo.r.References() + if err != nil { + return nil, err + } + + refs := make([]string, 0) + + err = refIter.ForEach(func(ref *plumbing.Reference) error { + if strings.HasPrefix(ref.Name().String(), refPrefix) { + refs = append(refs, ref.Name().String()) + } + return nil + }) + if err != nil { + return nil, err + } + + return refs, nil +} + +// RefExist will check if a reference exist in Git +func (repo *GoGitRepo) RefExist(ref string) (bool, error) { + _, err := repo.r.Reference(plumbing.ReferenceName(ref), false) + if err == nil { + return true, nil + } else if err == plumbing.ErrReferenceNotFound { + return false, nil + } + return false, err +} + +// CopyRef will create a new reference with the same value as another one +func (repo *GoGitRepo) CopyRef(source string, dest string) error { + r, err := repo.r.Reference(plumbing.ReferenceName(source), false) + if err != nil { + return err + } + return repo.r.Storer.SetReference(plumbing.NewHashReference(plumbing.ReferenceName(dest), r.Hash())) +} + +// ListCommits will return the list of tree hashes of a ref, in chronological order +func (repo *GoGitRepo) ListCommits(ref string) ([]Hash, error) { + r, err := repo.r.Reference(plumbing.ReferenceName(ref), false) + if err != nil { + return nil, err + } + + commit, err := repo.r.CommitObject(r.Hash()) + if err != nil { + return nil, err + } + hashes := []Hash{Hash(commit.Hash.String())} + + for { + commit, err = commit.Parent(0) + if err == object.ErrParentNotFound { + break + } + if err != nil { + return nil, err + } + + if commit.NumParents() > 1 { + return nil, fmt.Errorf("multiple parents") + } + + hashes = append([]Hash{Hash(commit.Hash.String())}, hashes...) + } + + return hashes, nil +} + +// GetOrCreateClock return a Lamport clock stored in the Repo. +// If the clock doesn't exist, it's created. +func (repo *GoGitRepo) GetOrCreateClock(name string) (lamport.Clock, error) { + c, err := repo.getClock(name) + if err == nil { + return c, nil + } + if err != ErrClockNotExist { + return nil, err + } + + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + p := stdpath.Join(repo.path, clockPath, name+"-clock") + + c, err = lamport.NewPersistedClock(p) + if err != nil { + return nil, err + } + + repo.clocks[name] = c + return c, nil +} + +func (repo *GoGitRepo) getClock(name string) (lamport.Clock, error) { + repo.clocksMutex.Lock() + defer repo.clocksMutex.Unlock() + + if c, ok := repo.clocks[name]; ok { + return c, nil + } + + p := stdpath.Join(repo.path, clockPath, name+"-clock") + + c, err := lamport.LoadPersistedClock(p) + if err == nil { + repo.clocks[name] = c + return c, nil + } + if err == lamport.ErrClockNotExist { + return nil, ErrClockNotExist + } + return nil, err +} + +// AddRemote add a new remote to the repository +// Not in the interface because it's only used for testing +func (repo *GoGitRepo) AddRemote(name string, url string) error { + _, err := repo.r.CreateRemote(&config.RemoteConfig{ + Name: name, + URLs: []string{url}, + }) + + return err +} |