2020-06-26 20:25:17 +03:00
package repository
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
2020-10-25 01:24:26 +03:00
"sort"
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
2022-02-15 23:35:49 +03:00
"github.com/ProtonMail/go-crypto/openpgp"
2020-12-08 16:42:13 +03:00
"github.com/blevesearch/bleve"
2020-12-01 23:19:23 +03:00
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
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"
2021-03-22 00:37:19 +03:00
"golang.org/x/sys/execabs"
2020-06-26 20:25:17 +03:00
"github.com/MichaelMure/git-bug/util/lamport"
)
2020-11-08 19:54:28 +03:00
const clockPath = "clocks"
2022-05-25 14:55:28 +03:00
const indexPath = "indexes"
2020-11-08 19:54:28 +03:00
2020-06-26 20:25:17 +03:00
var _ ClockedRepo = & GoGitRepo { }
2020-12-01 23:19:23 +03:00
var _ TestedRepo = & GoGitRepo { }
2020-06-26 20:25:17 +03:00
type GoGitRepo struct {
2021-04-17 21:35:35 +03:00
// Unfortunately, some parts of go-git are not thread-safe so we have to cover them with a big fat mutex here.
// See https://github.com/go-git/go-git/issues/48
// See https://github.com/go-git/go-git/issues/208
// See https://github.com/go-git/go-git/pull/186
rMutex sync . Mutex
r * gogit . Repository
path string
2020-06-26 20:25:17 +03:00
clocksMutex sync . Mutex
clocks map [ string ] lamport . Clock
2020-07-27 01:14:01 +03:00
2020-12-08 16:42:13 +03:00
indexesMutex sync . Mutex
indexes map [ string ] bleve . Index
2020-12-05 05:08:54 +03:00
keyring Keyring
localStorage billy . Filesystem
2020-06-26 20:25:17 +03:00
}
2022-05-26 20:39:13 +03:00
// OpenGoGitRepo opens an already existing repo at the given path and
// with the specified LocalStorage namespace. Given a repository path
// of "~/myrepo" and a namespace of "git-bug", local storage for the
// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
func OpenGoGitRepo ( path , namespace string , clockLoaders [ ] ClockLoader ) ( * GoGitRepo , error ) {
2020-06-26 20:25:17 +03:00
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-12-05 05:08:54 +03:00
r : r ,
path : path ,
clocks : make ( map [ string ] lamport . Clock ) ,
2020-12-08 16:42:13 +03:00
indexes : make ( map [ string ] bleve . Index ) ,
2020-12-05 05:08:54 +03:00
keyring : k ,
2022-05-26 20:39:13 +03:00
localStorage : osfs . New ( filepath . Join ( path , namespace ) ) ,
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
}
2022-05-26 20:39:13 +03:00
// InitGoGitRepo creates a new empty git repo at the given path and
// with the specified LocalStorage namespace. Given a repository path
// of "~/myrepo" and a namespace of "git-bug", local storage for the
// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
func InitGoGitRepo ( path , namespace string ) ( * GoGitRepo , error ) {
2020-12-05 05:08:54 +03:00
r , err := gogit . PlainInit ( path , false )
if err != nil {
return nil , err
}
k , err := defaultKeyring ( )
if err != nil {
return nil , err
}
return & GoGitRepo {
r : r ,
path : filepath . Join ( path , ".git" ) ,
clocks : make ( map [ string ] lamport . Clock ) ,
2020-12-08 16:42:13 +03:00
indexes : make ( map [ string ] bleve . Index ) ,
2020-12-05 05:08:54 +03:00
keyring : k ,
2022-05-26 20:39:13 +03:00
localStorage : osfs . New ( filepath . Join ( path , ".git" , namespace ) ) ,
2020-12-05 05:08:54 +03:00
} , nil
}
2022-05-26 20:39:13 +03:00
// InitBareGoGitRepo creates a new --bare empty git repo at the given
// path and with the specified LocalStorage namespace. Given a repository
// path of "~/myrepo" and a namespace of "git-bug", local storage for the
// GoGitRepo will be configured at "~/myrepo/.git/git-bug".
func InitBareGoGitRepo ( path , namespace string ) ( * GoGitRepo , error ) {
2020-12-05 05:08:54 +03:00
r , err := gogit . PlainInit ( path , true )
if err != nil {
return nil , err
}
k , err := defaultKeyring ( )
if err != nil {
return nil , err
}
return & GoGitRepo {
r : r ,
path : path ,
clocks : make ( map [ string ] lamport . Clock ) ,
2020-12-08 16:42:13 +03:00
indexes : make ( map [ string ] bleve . Index ) ,
2020-12-05 05:08:54 +03:00
keyring : k ,
2022-05-26 20:39:13 +03:00
localStorage : osfs . New ( filepath . Join ( path , namespace ) ) ,
2020-12-05 05:08:54 +03:00
} , nil
}
2020-06-26 20:25:17 +03:00
func detectGitPath ( path string ) ( string , error ) {
// normalize the path
path , err := filepath . Abs ( path )
if err != nil {
return "" , err
}
for {
2020-12-05 05:08:54 +03:00
fi , err := os . Stat ( filepath . Join ( path , ".git" ) )
2020-06-26 20:25:17 +03:00
if err == nil {
if ! fi . IsDir ( ) {
return "" , fmt . Errorf ( ".git exist but is not a directory" )
}
2020-12-05 05:08:54 +03:00
return filepath . Join ( path , ".git" ) , nil
2020-06-26 20:25:17 +03:00
}
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 {
2020-12-05 05:08:54 +03:00
_ , err := os . Stat ( filepath . Join ( path , marker ) )
2020-06-26 20:25:17 +03:00
if err == nil {
continue
}
if ! os . IsNotExist ( err ) {
// unknown error
return false , err
} else {
return false , nil
}
}
return true , nil
}
2020-12-08 16:42:13 +03:00
func ( repo * GoGitRepo ) Close ( ) error {
var firstErr error
for _ , index := range repo . indexes {
err := index . Close ( )
if err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
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-12-05 05:08:54 +03:00
return newGoGitGlobalConfig ( )
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
// GetUserName returns the name the the user has used to configure git
func ( repo * GoGitRepo ) GetUserName ( ) ( string , error ) {
2020-10-31 00:20:19 +03:00
return repo . AnyConfig ( ) . ReadString ( "user.name" )
2020-06-26 20:25:17 +03:00
}
// GetUserEmail returns the email address that the user has used to configure git.
func ( repo * GoGitRepo ) GetUserEmail ( ) ( string , error ) {
2020-10-31 00:20:19 +03:00
return repo . AnyConfig ( ) . ReadString ( "user.email" )
2020-06-26 20:25:17 +03:00
}
// 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 {
2021-03-22 00:37:19 +03:00
if _ , err = execabs . LookPath ( cmd ) ; err == nil {
2020-09-29 23:00:35 +03:00
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
}
2022-05-25 14:55:28 +03:00
// LocalStorage returns a billy.Filesystem giving access to
2022-05-26 20:39:13 +03:00
// $RepoPath/.git/$Namespace.
2020-12-01 23:19:23 +03:00
func ( repo * GoGitRepo ) LocalStorage ( ) billy . Filesystem {
2020-12-05 05:08:54 +03:00
return repo . localStorage
2020-12-01 23:19:23 +03:00
}
2020-12-08 16:42:13 +03:00
// GetBleveIndex return a bleve.Index that can be used to index documents
func ( repo * GoGitRepo ) GetBleveIndex ( name string ) ( bleve . Index , error ) {
repo . indexesMutex . Lock ( )
defer repo . indexesMutex . Unlock ( )
if index , ok := repo . indexes [ name ] ; ok {
return index , nil
}
2022-05-25 14:55:28 +03:00
path := filepath . Join ( repo . localStorage . Root ( ) , indexPath , name )
2020-12-08 16:42:13 +03:00
index , err := bleve . Open ( path )
if err == nil {
repo . indexes [ name ] = index
return index , nil
}
err = os . MkdirAll ( path , os . ModePerm )
if err != nil {
return nil , err
}
mapping := bleve . NewIndexMapping ( )
mapping . DefaultAnalyzer = "en"
index , err = bleve . New ( path , mapping )
if err != nil {
return nil , err
}
repo . indexes [ name ] = index
return index , nil
}
// ClearBleveIndex will wipe the given index
func ( repo * GoGitRepo ) ClearBleveIndex ( name string ) error {
repo . indexesMutex . Lock ( )
defer repo . indexesMutex . Unlock ( )
if index , ok := repo . indexes [ name ] ; ok {
2022-05-31 13:24:58 +03:00
err := index . Close ( )
2020-12-08 16:42:13 +03:00
if err != nil {
return err
}
delete ( repo . indexes , name )
}
2022-05-31 13:24:58 +03:00
path := filepath . Join ( repo . localStorage . Root ( ) , indexPath , name )
err := os . RemoveAll ( path )
if err != nil {
return err
}
2020-12-08 16:42:13 +03:00
return nil
}
2021-02-05 13:18:38 +03:00
// FetchRefs fetch git refs matching a directory prefix to a remote
// Ex: prefix="foo" will fetch any remote refs matching "refs/foo/*" locally.
// The equivalent git refspec would be "refs/foo/*:refs/remotes/<remote>/foo/*"
func ( repo * GoGitRepo ) FetchRefs ( remote string , prefix string ) ( string , error ) {
refspec := fmt . Sprintf ( "refs/%s/*:refs/remotes/%s/%s/*" , prefix , remote , prefix )
2020-06-26 20:25:17 +03:00
buf := bytes . NewBuffer ( nil )
err := repo . r . Fetch ( & gogit . FetchOptions {
RemoteName : remote ,
2021-02-05 13:18:38 +03:00
RefSpecs : [ ] config . RefSpec { config . RefSpec ( refspec ) } ,
2020-06-26 20:25:17 +03:00
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
}
2021-02-05 13:18:38 +03:00
// PushRefs push git refs matching a directory prefix to a remote
// Ex: prefix="foo" will push any local refs matching "refs/foo/*" to the remote.
// The equivalent git refspec would be "refs/foo/*:refs/foo/*"
//
// Additionally, PushRefs will update the local references in refs/remotes/<remote>/foo to match
// the remote state.
func ( repo * GoGitRepo ) PushRefs ( remote string , prefix string ) ( string , error ) {
refspec := fmt . Sprintf ( "refs/%s/*:refs/%s/*" , prefix , prefix )
remo , err := repo . r . Remote ( remote )
if err != nil {
return "" , err
}
// to make sure that the push also create the corresponding refs/remotes/<remote>/... references,
// we need to have a default fetch refspec configured on the remote, to make our refs "track" the remote ones.
// This does not change the config on disk, only on memory.
hasCustomFetch := false
fetchRefspec := fmt . Sprintf ( "refs/%s/*:refs/remotes/%s/%s/*" , prefix , remote , prefix )
for _ , r := range remo . Config ( ) . Fetch {
if string ( r ) == fetchRefspec {
hasCustomFetch = true
break
}
}
if ! hasCustomFetch {
remo . Config ( ) . Fetch = append ( remo . Config ( ) . Fetch , config . RefSpec ( fetchRefspec ) )
}
2020-06-26 20:25:17 +03:00
buf := bytes . NewBuffer ( nil )
2021-02-05 13:18:38 +03:00
err = remo . Push ( & gogit . PushOptions {
2020-06-26 20:25:17 +03:00
RemoteName : remote ,
2021-02-05 13:18:38 +03:00
RefSpecs : [ ] config . RefSpec { config . RefSpec ( refspec ) } ,
2020-06-26 20:25:17 +03:00
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 ) {
2021-04-17 21:35:35 +03:00
repo . rMutex . Lock ( )
defer repo . rMutex . Unlock ( )
2020-06-26 20:25:17 +03:00
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
2020-10-25 01:24:26 +03:00
// TODO: can be removed once https://github.com/go-git/go-git/issues/193 is resolved
sorted := make ( [ ] TreeEntry , len ( mapping ) )
copy ( sorted , mapping )
sort . Slice ( sorted , func ( i , j int ) bool {
nameI := sorted [ i ] . Name
if sorted [ i ] . ObjectType == Tree {
nameI += "/"
}
nameJ := sorted [ j ] . Name
if sorted [ j ] . ObjectType == Tree {
nameJ += "/"
}
return nameI < nameJ
} )
for _ , entry := range sorted {
2020-09-02 15:48:28 +03:00
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 ) {
2021-04-22 13:51:07 +03:00
repo . rMutex . Lock ( )
defer repo . rMutex . Unlock ( )
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
2021-01-04 01:59:25 +03:00
func ( repo * GoGitRepo ) StoreCommit ( treeHash Hash , parents ... Hash ) ( Hash , error ) {
return repo . StoreSignedCommit ( treeHash , nil , parents ... )
2020-06-26 20:25:17 +03:00
}
2022-05-31 13:24:58 +03:00
// StoreSignedCommit will store a Git commit with the given Git tree. If signKey is not nil, the commit
2021-01-04 01:59:25 +03:00
// will be signed accordingly.
func ( repo * GoGitRepo ) StoreSignedCommit ( treeHash Hash , signKey * openpgp . Entity , parents ... 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 ( ) ) ,
}
2021-01-04 01:59:25 +03:00
for _ , parent := range parents {
commit . ParentHashes = append ( commit . ParentHashes , plumbing . NewHash ( parent . String ( ) ) )
}
// Compute the signature if needed
if signKey != nil {
// first get the serialized commit
encoded := & plumbing . MemoryObject { }
if err := commit . Encode ( encoded ) ; err != nil {
return "" , err
}
r , err := encoded . Reader ( )
if err != nil {
return "" , err
}
// sign the data
var sig bytes . Buffer
if err := openpgp . ArmoredDetachSign ( & sig , signKey , r , nil ) ; err != nil {
return "" , err
}
commit . PGPSignature = sig . String ( )
2020-09-08 06:00:36 +03:00
}
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 ) {
2021-04-22 13:51:07 +03:00
repo . rMutex . Lock ( )
defer repo . rMutex . Unlock ( )
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 ) {
2021-04-22 13:51:07 +03:00
repo . rMutex . Lock ( )
defer repo . rMutex . Unlock ( )
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-12-21 13:05:47 +03:00
func ( repo * GoGitRepo ) ResolveRef ( ref string ) ( Hash , error ) {
r , err := repo . r . Reference ( plumbing . ReferenceName ( ref ) , false )
if err != nil {
return "" , err
}
return Hash ( r . Hash ( ) . String ( ) ) , nil
}
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 ) {
2021-01-04 01:59:25 +03:00
return nonNativeListCommits ( repo , ref )
}
2020-09-08 06:00:36 +03:00
2021-01-04 01:59:25 +03:00
func ( repo * GoGitRepo ) ReadCommit ( hash Hash ) ( Commit , error ) {
2021-04-22 13:51:07 +03:00
repo . rMutex . Lock ( )
defer repo . rMutex . Unlock ( )
2021-02-09 12:46:33 +03:00
commit , err := repo . r . CommitObject ( plumbing . NewHash ( hash . String ( ) ) )
2020-12-21 13:05:47 +03:00
if err != nil {
return Commit { } , err
}
parents := make ( [ ] Hash , len ( commit . ParentHashes ) )
for i , parentHash := range commit . ParentHashes {
parents [ i ] = Hash ( parentHash . String ( ) )
}
2021-01-04 01:59:25 +03:00
result := Commit {
2020-12-21 13:05:47 +03:00
Hash : hash ,
Parents : parents ,
TreeHash : Hash ( commit . TreeHash . String ( ) ) ,
2021-01-04 01:59:25 +03:00
}
if commit . PGPSignature != "" {
2021-02-09 12:46:33 +03:00
// I can't find a way to just remove the signature when reading the encoded commit so we need to
// re-encode the commit without signature.
encoded := & plumbing . MemoryObject { }
err := commit . EncodeWithoutSignature ( encoded )
if err != nil {
return Commit { } , err
}
2021-01-04 01:59:25 +03:00
result . SignedData , err = encoded . Reader ( )
if err != nil {
return Commit { } , err
}
2020-12-21 13:05:47 +03:00
2021-01-04 01:59:25 +03:00
result . Signature , err = deArmorSignature ( strings . NewReader ( commit . PGPSignature ) )
if err != nil {
return Commit { } , err
}
}
return result , nil
2020-12-21 13:05:47 +03:00
}
2020-11-08 19:54:28 +03:00
func ( repo * GoGitRepo ) AllClocks ( ) ( map [ string ] lamport . Clock , error ) {
repo . clocksMutex . Lock ( )
defer repo . clocksMutex . Unlock ( )
result := make ( map [ string ] lamport . Clock )
2022-05-25 14:55:28 +03:00
files , err := ioutil . ReadDir ( filepath . Join ( repo . localStorage . Root ( ) , clockPath ) )
2020-11-08 19:54:28 +03:00
if os . IsNotExist ( err ) {
return nil , nil
}
if err != nil {
return nil , err
}
for _ , file := range files {
name := file . Name ( )
if c , ok := repo . clocks [ name ] ; ok {
result [ name ] = c
} else {
c , err := lamport . LoadPersistedClock ( repo . LocalStorage ( ) , filepath . Join ( clockPath , name ) )
if err != nil {
return nil , err
}
repo . clocks [ name ] = c
result [ name ] = c
}
}
return result , 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 ) {
2020-12-08 15:15:21 +03:00
repo . clocksMutex . Lock ( )
defer repo . clocksMutex . Unlock ( )
2020-06-26 20:25:17 +03:00
c , err := repo . getClock ( name )
if err == nil {
return c , nil
}
if err != ErrClockNotExist {
return nil , err
}
2020-11-08 19:54:28 +03:00
c , err = lamport . NewPersistedClock ( repo . LocalStorage ( ) , filepath . Join ( clockPath , name ) )
2020-06-26 20:25:17 +03:00
if err != nil {
return nil , err
}
repo . clocks [ name ] = c
return c , nil
}
func ( repo * GoGitRepo ) getClock ( name string ) ( lamport . Clock , error ) {
if c , ok := repo . clocks [ name ] ; ok {
return c , nil
}
2020-11-08 19:54:28 +03:00
c , err := lamport . LoadPersistedClock ( repo . LocalStorage ( ) , filepath . Join ( clockPath , name ) )
2020-06-26 20:25:17 +03:00
if err == nil {
repo . clocks [ name ] = c
return c , nil
}
if err == lamport . ErrClockNotExist {
return nil , ErrClockNotExist
}
return nil , err
}
2020-11-08 19:54:28 +03:00
// Increment is equivalent to c = GetOrCreateClock(name) + c.Increment()
func ( repo * GoGitRepo ) Increment ( name string ) ( lamport . Time , error ) {
c , err := repo . GetOrCreateClock ( name )
if err != nil {
return lamport . Time ( 0 ) , err
}
return c . Increment ( )
}
// Witness is equivalent to c = GetOrCreateClock(name) + c.Witness(time)
func ( repo * GoGitRepo ) Witness ( name string , time lamport . Time ) error {
c , err := repo . GetOrCreateClock ( name )
if err != nil {
return err
}
return c . Witness ( time )
}
2020-06-26 20:25:17 +03:00
// 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
}
2020-12-01 23:19:23 +03:00
// GetLocalRemote return the URL to use to add this repo as a local remote
func ( repo * GoGitRepo ) GetLocalRemote ( ) string {
return repo . path
}
// EraseFromDisk delete this repository entirely from the disk
func ( repo * GoGitRepo ) EraseFromDisk ( ) error {
2020-12-08 17:09:58 +03:00
err := repo . Close ( )
if err != nil {
return err
}
2020-12-01 23:19:23 +03:00
path := filepath . Clean ( strings . TrimSuffix ( repo . path , string ( filepath . Separator ) + ".git" ) )
// fmt.Println("Cleaning repo:", path)
return os . RemoveAll ( path )
}