2020-06-26 20:25:17 +03:00
package repository
import (
"bytes"
"fmt"
"io/ioutil"
"os"
2020-09-29 23:00:35 +03:00
"os/exec"
2020-06-26 20:25:17 +03:00
stdpath "path"
"path/filepath"
2020-09-08 15:31:40 +03:00
"strings"
2020-06-26 20:25:17 +03:00
"sync"
2020-09-08 06:00:36 +03:00
"time"
2020-06-26 20:25:17 +03:00
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
2020-09-02 15:48:28 +03:00
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
2020-06-26 20:25:17 +03:00
"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
2020-07-27 01:14:01 +03:00
keyring Keyring
2020-06-26 20:25:17 +03:00
}
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
}
2020-07-27 01:14:01 +03:00
k , err := defaultKeyring ( )
if err != nil {
return nil , err
}
2020-06-26 20:25:17 +03:00
repo := & GoGitRepo {
2020-07-27 01:14:01 +03:00
r : r ,
path : path ,
clocks : make ( map [ string ] lamport . Clock ) ,
keyring : k ,
2020-06-26 20:25:17 +03:00
}
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
}
2020-10-04 21:03:44 +03:00
k , err := defaultKeyring ( )
if err != nil {
return nil , err
}
2020-06-26 20:25:17 +03:00
return & GoGitRepo {
2020-10-04 21:03:44 +03:00
r : r ,
path : path + "/.git" ,
clocks : make ( map [ string ] lamport . Clock ) ,
keyring : k ,
2020-06-26 20:25:17 +03:00
} , 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
}
2020-10-04 21:03:44 +03:00
k , err := defaultKeyring ( )
if err != nil {
return nil , err
}
2020-06-26 20:25:17 +03:00
return & GoGitRepo {
2020-10-04 21:03:44 +03:00
r : r ,
path : path ,
clocks : make ( map [ string ] lamport . Clock ) ,
keyring : k ,
2020-06-26 20:25:17 +03:00
} , nil
}
2020-09-27 01:54:14 +03:00
// LocalConfig give access to the repository scoped configuration
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) LocalConfig ( ) Config {
2020-09-27 21:31:09 +03:00
return newGoGitLocalConfig ( repo . r )
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// GlobalConfig give access to the global scoped configuration
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) GlobalConfig ( ) Config {
2020-09-27 01:54:14 +03:00
// 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
2020-09-27 21:31:09 +03:00
return newGoGitGlobalConfig ( repo . r )
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// 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
2020-07-27 01:14:01 +03:00
func ( repo * GoGitRepo ) Keyring ( ) Keyring {
return repo . keyring
}
2020-06-26 20:25:17 +03:00
// 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 ) {
2020-09-28 00:14:51 +03:00
// 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.
2020-06-26 20:25:17 +03:00
2020-09-28 00:14:51 +03:00
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
}
2020-09-29 23:00:35 +03:00
priorities := [ ] string {
"editor" ,
"nano" ,
"vim" ,
"vi" ,
"emacs" ,
}
for _ , cmd := range priorities {
if _ , err = exec . LookPath ( cmd ) ; err == nil {
return cmd , nil
}
}
return "ed" , nil
2020-06-26 20:25:17 +03:00
}
// 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 ,
} )
2020-10-04 20:56:16 +03:00
if err == gogit . NoErrAlreadyUpToDate {
return "already up-to-date" , nil
}
2020-06-26 20:25:17 +03:00
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 ,
} )
2020-10-04 20:56:16 +03:00
if err == gogit . NoErrAlreadyUpToDate {
return "already up-to-date" , nil
}
2020-06-26 20:25:17 +03:00
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 )
}
2020-09-27 01:54:14 +03:00
// StoreTree will store a mapping key-->Hash as a Git tree
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) StoreTree ( mapping [ ] TreeEntry ) ( Hash , error ) {
2020-09-02 15:48:28 +03:00
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 ( )
2020-09-08 06:00:36 +03:00
obj . SetType ( plumbing . TreeObject )
2020-09-02 15:48:28 +03:00
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
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// ReadTree will return the list of entries in a Git tree
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) ReadTree ( hash Hash ) ( [ ] TreeEntry , error ) {
2020-09-27 22:14:25 +03:00
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" )
}
2020-09-08 06:00:36 +03:00
if err != nil {
return nil , err
}
2020-09-27 22:14:25 +03:00
treeEntries := make ( [ ] TreeEntry , len ( tree . Entries ) )
for i , entry := range tree . Entries {
2020-09-08 06:00:36 +03:00
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
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// StoreCommit will store a Git commit with the given Git tree
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) StoreCommit ( treeHash Hash ) ( Hash , error ) {
2020-09-08 06:00:36 +03:00
return repo . StoreCommitWithParent ( treeHash , "" )
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// StoreCommit will store a Git commit with the given Git tree
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) StoreCommitWithParent ( treeHash Hash , parent Hash ) ( Hash , error ) {
2020-09-08 06:00:36 +03:00
cfg , err := repo . r . Config ( )
if err != nil {
return "" , err
}
commit := object . Commit {
Author : object . Signature {
2020-10-04 21:46:38 +03:00
Name : cfg . Author . Name ,
Email : cfg . Author . Email ,
When : time . Now ( ) ,
2020-09-08 06:00:36 +03:00
} ,
Committer : object . Signature {
2020-10-04 21:46:38 +03:00
Name : cfg . Committer . Name ,
Email : cfg . Committer . Email ,
When : time . Now ( ) ,
2020-09-08 06:00:36 +03:00
} ,
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
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// GetTreeHash return the git tree hash referenced in a commit
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) GetTreeHash ( commit Hash ) ( Hash , error ) {
2020-09-08 06:00:36 +03:00
obj , err := repo . r . CommitObject ( plumbing . NewHash ( commit . String ( ) ) )
if err != nil {
return "" , err
}
return Hash ( obj . TreeHash . String ( ) ) , nil
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// FindCommonAncestor will return the last common ancestor of two chain of commit
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) FindCommonAncestor ( commit1 Hash , commit2 Hash ) ( Hash , error ) {
2020-09-08 06:00:36 +03:00
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
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// UpdateRef will create or update a Git reference
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) UpdateRef ( ref string , hash Hash ) error {
2020-09-08 06:00:36 +03:00
return repo . r . Storer . SetReference ( plumbing . NewHashReference ( plumbing . ReferenceName ( ref ) , plumbing . NewHash ( hash . String ( ) ) ) )
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// RemoveRef will remove a Git reference
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) RemoveRef ( ref string ) error {
2020-09-02 15:48:28 +03:00
return repo . r . Storer . RemoveReference ( plumbing . ReferenceName ( ref ) )
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// ListRefs will return a list of Git ref matching the given refspec
2020-09-08 15:31:40 +03:00
func ( repo * GoGitRepo ) ListRefs ( refPrefix string ) ( [ ] string , error ) {
2020-09-08 06:00:36 +03:00
refIter , err := repo . r . References ( )
if err != nil {
return nil , err
}
refs := make ( [ ] string , 0 )
2020-09-08 15:31:40 +03:00
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
2020-09-08 06:00:36 +03:00
}
2020-09-08 15:31:40 +03:00
2020-09-08 06:00:36 +03:00
return refs , nil
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// RefExist will check if a reference exist in Git
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) RefExist ( ref string ) ( bool , error ) {
2020-09-08 06:00:36 +03:00
_ , 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
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// CopyRef will create a new reference with the same value as another one
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) CopyRef ( source string , dest string ) error {
2020-09-08 15:45:03 +03:00
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 ( ) ) )
2020-06-26 20:25:17 +03:00
}
2020-09-27 01:54:14 +03:00
// ListCommits will return the list of tree hashes of a ref, in chronological order
2020-06-26 20:25:17 +03:00
func ( repo * GoGitRepo ) ListCommits ( ref string ) ( [ ] Hash , error ) {
2020-09-08 15:45:03 +03:00
r , err := repo . r . Reference ( plumbing . ReferenceName ( ref ) , false )
2020-09-08 06:00:36 +03:00
if err != nil {
return nil , err
}
2020-09-08 15:45:03 +03:00
commit , err := repo . r . CommitObject ( r . Hash ( ) )
2020-09-08 15:31:40 +03:00
if err != nil {
return nil , err
2020-09-08 06:00:36 +03:00
}
2020-09-29 21:16:15 +03:00
hashes := [ ] Hash { Hash ( commit . Hash . String ( ) ) }
2020-09-08 15:45:03 +03:00
for {
commit , err = commit . Parent ( 0 )
2020-09-29 21:16:15 +03:00
if err == object . ErrParentNotFound {
break
}
2020-09-08 15:45:03 +03:00
if err != nil {
return nil , err
}
if commit . NumParents ( ) > 1 {
return nil , fmt . Errorf ( "multiple parents" )
}
2020-09-29 21:16:15 +03:00
hashes = append ( [ ] Hash { Hash ( commit . Hash . String ( ) ) } , hashes ... )
2020-09-08 15:45:03 +03:00
}
2020-09-08 15:31:40 +03:00
2020-09-29 21:16:15 +03:00
return hashes , nil
2020-06-26 20:25:17 +03:00
}
// 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 ( )
2020-09-26 15:05:11 +03:00
p := stdpath . Join ( repo . path , clockPath , name + "-clock" )
2020-06-26 20:25:17 +03:00
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
}
2020-09-26 15:05:11 +03:00
p := stdpath . Join ( repo . path , clockPath , name + "-clock" )
2020-06-26 20:25:17 +03:00
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
}