2017-07-07 21:35:10 +00:00
package git
import (
"bytes"
2018-01-16 22:30:10 +00:00
"context"
2017-07-25 19:11:52 +00:00
"fmt"
2017-07-07 21:35:10 +00:00
"io"
2017-07-08 23:25:07 +00:00
"os"
2017-07-07 21:35:10 +00:00
"os/exec"
2017-07-08 23:25:07 +00:00
"path/filepath"
2017-07-07 21:35:10 +00:00
"regexp"
"strings"
2018-06-08 18:00:37 +00:00
"github.com/docker/docker/pkg/locker"
2017-07-07 21:35:10 +00:00
"github.com/moby/buildkit/cache"
"github.com/moby/buildkit/cache/metadata"
2018-07-26 19:07:52 +00:00
"github.com/moby/buildkit/client"
2017-07-08 23:25:07 +00:00
"github.com/moby/buildkit/identity"
2019-02-23 12:56:04 +00:00
"github.com/moby/buildkit/session"
2017-07-07 21:35:10 +00:00
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/source"
"github.com/moby/buildkit/util/progress/logs"
"github.com/pkg/errors"
2017-07-19 01:05:19 +00:00
"github.com/sirupsen/logrus"
2018-09-18 18:18:08 +00:00
bolt "go.etcd.io/bbolt"
2017-07-07 21:35:10 +00:00
)
var validHex = regexp . MustCompile ( ` ^[a-f0-9] { 40}$ ` )
type Opt struct {
CacheAccessor cache . Accessor
MetadataStore * metadata . Store
}
type gitSource struct {
md * metadata . Store
cache cache . Accessor
locker * locker . Locker
}
2018-11-28 09:43:43 +00:00
// Supported returns nil if the system supports Git source
func Supported ( ) error {
if err := exec . Command ( "git" , "version" ) . Run ( ) ; err != nil {
return errors . Wrap ( err , "failed to find git binary" )
}
return nil
}
2017-07-07 21:35:10 +00:00
func NewSource ( opt Opt ) ( source . Source , error ) {
gs := & gitSource {
md : opt . MetadataStore ,
cache : opt . CacheAccessor ,
2018-06-08 18:00:37 +00:00
locker : locker . New ( ) ,
2017-07-07 21:35:10 +00:00
}
return gs , nil
}
func ( gs * gitSource ) ID ( ) string {
return source . GitScheme
}
// needs to be called with repo lock
func ( gs * gitSource ) mountRemote ( ctx context . Context , remote string ) ( target string , release func ( ) , retErr error ) {
remoteKey := "git-remote::" + remote
sis , err := gs . md . Search ( remoteKey )
if err != nil {
return "" , nil , errors . Wrapf ( err , "failed to search metadata for %s" , remote )
}
var remoteRef cache . MutableRef
for _ , si := range sis {
remoteRef , err = gs . cache . GetMutable ( ctx , si . ID ( ) )
if err != nil {
if cache . IsLocked ( err ) {
// should never really happen as no other function should access this metadata, but lets be graceful
logrus . Warnf ( "mutable ref for %s %s was locked: %v" , remote , si . ID ( ) , err )
continue
}
return "" , nil , errors . Wrapf ( err , "failed to get mutable ref for %s" , remote )
}
break
}
initializeRepo := false
if remoteRef == nil {
2017-07-25 19:11:52 +00:00
remoteRef , err = gs . cache . New ( ctx , nil , cache . CachePolicyRetain , cache . WithDescription ( fmt . Sprintf ( "shared git repo for %s" , remote ) ) )
2017-07-07 21:35:10 +00:00
if err != nil {
return "" , nil , errors . Wrapf ( err , "failed to create new mutable for %s" , remote )
}
initializeRepo = true
}
releaseRemoteRef := func ( ) {
2017-07-14 18:59:31 +00:00
remoteRef . Release ( context . TODO ( ) )
2017-07-07 21:35:10 +00:00
}
defer func ( ) {
if retErr != nil && remoteRef != nil {
releaseRemoteRef ( )
}
} ( )
2017-07-18 06:08:22 +00:00
mount , err := remoteRef . Mount ( ctx , false )
2017-07-07 21:35:10 +00:00
if err != nil {
return "" , nil , err
}
lm := snapshot . LocalMounter ( mount )
dir , err := lm . Mount ( )
if err != nil {
return "" , nil , err
}
defer func ( ) {
if retErr != nil {
lm . Unmount ( )
}
} ( )
if initializeRepo {
if _ , err := gitWithinDir ( ctx , dir , "" , "init" , "--bare" ) ; err != nil {
return "" , nil , errors . Wrapf ( err , "failed to init repo at %s" , dir )
}
if _ , err := gitWithinDir ( ctx , dir , "" , "remote" , "add" , "origin" , remote ) ; err != nil {
return "" , nil , errors . Wrapf ( err , "failed add origin repo at %s" , dir )
}
// same new remote metadata
si , _ := gs . md . Get ( remoteRef . ID ( ) )
v , err := metadata . NewValue ( remoteKey )
v . Index = remoteKey
if err != nil {
return "" , nil , err
}
if err := si . Update ( func ( b * bolt . Bucket ) error {
2017-07-14 18:59:31 +00:00
return si . SetValue ( b , "git-remote" , v )
2017-07-07 21:35:10 +00:00
} ) ; err != nil {
return "" , nil , err
}
}
return dir , func ( ) {
lm . Unmount ( )
releaseRemoteRef ( )
} , nil
}
type gitSourceHandler struct {
* gitSource
2017-07-08 23:25:07 +00:00
src source . GitIdentifier
2017-07-07 21:35:10 +00:00
cacheKey string
}
2019-02-23 12:56:04 +00:00
func ( gs * gitSource ) Resolve ( ctx context . Context , id source . Identifier , _ * session . Manager ) ( source . SourceInstance , error ) {
2017-07-07 21:35:10 +00:00
gitIdentifier , ok := id . ( * source . GitIdentifier )
if ! ok {
return nil , errors . Errorf ( "invalid git identifier %v" , id )
}
return & gitSourceHandler {
2017-07-08 23:25:07 +00:00
src : * gitIdentifier ,
2017-07-07 21:35:10 +00:00
gitSource : gs ,
} , nil
}
2018-04-24 21:00:58 +00:00
func ( gs * gitSourceHandler ) CacheKey ( ctx context . Context , index int ) ( string , bool , error ) {
2017-07-07 21:35:10 +00:00
remote := gs . src . Remote
ref := gs . src . Ref
if ref == "" {
ref = "master"
}
gs . locker . Lock ( remote )
defer gs . locker . Unlock ( remote )
if isCommitSHA ( ref ) {
gs . cacheKey = ref
2018-04-25 22:55:49 +00:00
return ref , true , nil
2017-07-07 21:35:10 +00:00
}
gitDir , unmountGitDir , err := gs . mountRemote ( ctx , remote )
if err != nil {
2018-04-24 21:00:58 +00:00
return "" , false , err
2017-07-07 21:35:10 +00:00
}
defer unmountGitDir ( )
// TODO: should we assume that remote tag is immutable? add a timer?
buf , err := gitWithinDir ( ctx , gitDir , "" , "ls-remote" , "origin" , ref )
if err != nil {
2018-04-24 21:00:58 +00:00
return "" , false , errors . Wrapf ( err , "failed to fetch remote %s" , remote )
2017-07-07 21:35:10 +00:00
}
out := buf . String ( )
idx := strings . Index ( out , "\t" )
if idx == - 1 {
2018-04-24 21:00:58 +00:00
return "" , false , errors . Errorf ( "failed to find commit SHA from output: %s" , string ( out ) )
2017-07-07 21:35:10 +00:00
}
sha := string ( out [ : idx ] )
if ! isCommitSHA ( sha ) {
2018-04-24 21:00:58 +00:00
return "" , false , errors . Errorf ( "invalid commit sha %q" , sha )
2017-07-07 21:35:10 +00:00
}
2017-07-08 23:25:07 +00:00
gs . cacheKey = sha
2018-04-24 21:00:58 +00:00
return sha , true , nil
2017-07-07 21:35:10 +00:00
}
func ( gs * gitSourceHandler ) Snapshot ( ctx context . Context ) ( out cache . ImmutableRef , retErr error ) {
ref := gs . src . Ref
if ref == "" {
ref = "master"
}
cacheKey := gs . cacheKey
if cacheKey == "" {
var err error
2018-04-24 21:00:58 +00:00
cacheKey , _ , err = gs . CacheKey ( ctx , 0 )
2017-07-07 21:35:10 +00:00
if err != nil {
return nil , err
}
}
snapshotKey := "git-snapshot::" + cacheKey + ":" + gs . src . Subdir
gs . locker . Lock ( snapshotKey )
defer gs . locker . Unlock ( snapshotKey )
sis , err := gs . md . Search ( snapshotKey )
if err != nil {
return nil , errors . Wrapf ( err , "failed to search metadata for %s" , snapshotKey )
}
if len ( sis ) > 0 {
return gs . cache . Get ( ctx , sis [ 0 ] . ID ( ) )
}
2017-07-21 17:58:24 +00:00
gs . locker . Lock ( gs . src . Remote )
defer gs . locker . Unlock ( gs . src . Remote )
2017-07-07 21:35:10 +00:00
gitDir , unmountGitDir , err := gs . mountRemote ( ctx , gs . src . Remote )
if err != nil {
return nil , err
}
defer unmountGitDir ( )
doFetch := true
if isCommitSHA ( ref ) {
// skip fetch if commit already exists
if _ , err := gitWithinDir ( ctx , gitDir , "" , "cat-file" , "-e" , ref + "^{commit}" ) ; err == nil {
doFetch = false
}
}
if doFetch {
2019-01-22 22:04:54 +00:00
// make sure no old lock files have leaked
os . RemoveAll ( filepath . Join ( gitDir , "shallow.lock" ) )
2017-12-09 18:09:23 +00:00
args := [ ] string { "fetch" }
2017-07-07 21:35:10 +00:00
if ! isCommitSHA ( ref ) { // TODO: find a branch from ls-remote?
args = append ( args , "--depth=1" , "--no-tags" )
2017-07-08 23:25:07 +00:00
} else {
if _ , err := os . Lstat ( filepath . Join ( gitDir , "shallow" ) ) ; err == nil {
args = append ( args , "--unshallow" )
}
2017-07-07 21:35:10 +00:00
}
args = append ( args , "origin" )
if ! isCommitSHA ( ref ) {
2017-07-08 23:25:07 +00:00
args = append ( args , ref + ":tags/" + ref )
2017-07-07 21:35:10 +00:00
// local refs are needed so they would be advertised on next fetches
// TODO: is there a better way to do this?
}
if _ , err := gitWithinDir ( ctx , gitDir , "" , args ... ) ; err != nil {
return nil , errors . Wrapf ( err , "failed to fetch remote %s" , gs . src . Remote )
}
}
2018-07-26 19:07:52 +00:00
checkoutRef , err := gs . cache . New ( ctx , nil , cache . WithRecordType ( client . UsageRecordTypeGitCheckout ) , cache . WithDescription ( fmt . Sprintf ( "git snapshot for %s#%s" , gs . src . Remote , ref ) ) )
2017-07-07 21:35:10 +00:00
if err != nil {
return nil , errors . Wrapf ( err , "failed to create new mutable for %s" , gs . src . Remote )
}
defer func ( ) {
if retErr != nil && checkoutRef != nil {
2017-07-14 18:59:31 +00:00
checkoutRef . Release ( context . TODO ( ) )
2017-07-07 21:35:10 +00:00
}
} ( )
2017-07-18 06:08:22 +00:00
mount , err := checkoutRef . Mount ( ctx , false )
2017-07-07 21:35:10 +00:00
if err != nil {
return nil , err
}
lm := snapshot . LocalMounter ( mount )
checkoutDir , err := lm . Mount ( )
if err != nil {
return nil , err
}
2017-07-08 23:25:07 +00:00
defer func ( ) {
if retErr != nil && lm != nil {
lm . Unmount ( )
}
} ( )
2017-07-07 21:35:10 +00:00
2017-07-08 23:25:07 +00:00
if gs . src . KeepGitDir {
_ , err = gitWithinDir ( ctx , checkoutDir , "" , "init" )
if err != nil {
return nil , err
}
_ , err = gitWithinDir ( ctx , checkoutDir , "" , "remote" , "add" , "origin" , gitDir )
if err != nil {
return nil , err
}
pullref := ref
if isCommitSHA ( ref ) {
pullref = "refs/buildkit/" + identity . NewID ( )
_ , err = gitWithinDir ( ctx , gitDir , "" , "update-ref" , pullref , ref )
if err != nil {
return nil , err
}
}
2017-12-09 18:09:23 +00:00
_ , err = gitWithinDir ( ctx , checkoutDir , "" , "fetch" , "--depth=1" , "origin" , pullref )
2017-07-08 23:25:07 +00:00
if err != nil {
return nil , err
}
_ , err = gitWithinDir ( ctx , checkoutDir , checkoutDir , "checkout" , "FETCH_HEAD" )
if err != nil {
return nil , errors . Wrapf ( err , "failed to checkout remote %s" , gs . src . Remote )
}
2017-12-09 18:09:23 +00:00
gitDir = checkoutDir
2017-07-08 23:25:07 +00:00
} else {
_ , err = gitWithinDir ( ctx , gitDir , checkoutDir , "checkout" , ref , "--" , "." )
if err != nil {
return nil , errors . Wrapf ( err , "failed to checkout remote %s" , gs . src . Remote )
}
2017-07-07 21:35:10 +00:00
}
2017-12-09 18:09:23 +00:00
_ , err = gitWithinDir ( ctx , gitDir , checkoutDir , "submodule" , "update" , "--init" , "--recursive" , "--depth=1" )
if err != nil {
return nil , errors . Wrapf ( err , "failed to update submodules for %s" , gs . src . Remote )
}
2019-03-20 21:54:20 +00:00
if idmap := mount . IdentityMapping ( ) ; idmap != nil {
u := idmap . RootPair ( )
err := filepath . Walk ( gitDir , func ( p string , f os . FileInfo , err error ) error {
return os . Lchown ( p , u . UID , u . GID )
} )
if err != nil {
return nil , errors . Wrap ( err , "failed to remap git checkout" )
}
}
2017-07-08 23:25:07 +00:00
lm . Unmount ( )
lm = nil
2017-07-07 21:35:10 +00:00
2017-07-14 18:59:31 +00:00
snap , err := checkoutRef . Commit ( ctx )
2017-07-07 21:35:10 +00:00
if err != nil {
return nil , err
}
checkoutRef = nil
defer func ( ) {
if retErr != nil {
snap . Release ( context . TODO ( ) )
}
} ( )
si , _ := gs . md . Get ( snap . ID ( ) )
v , err := metadata . NewValue ( snapshotKey )
v . Index = snapshotKey
if err != nil {
return nil , err
}
if err := si . Update ( func ( b * bolt . Bucket ) error {
2017-07-14 18:59:31 +00:00
return si . SetValue ( b , "git-snapshot" , v )
2017-07-07 21:35:10 +00:00
} ) ; err != nil {
return nil , err
}
return snap , nil
}
func isCommitSHA ( str string ) bool {
return validHex . MatchString ( str )
}
func gitWithinDir ( ctx context . Context , gitDir , workDir string , args ... string ) ( * bytes . Buffer , error ) {
a := [ ] string { "--git-dir" , gitDir }
if workDir != "" {
a = append ( a , "--work-tree" , workDir )
}
2017-12-09 18:09:23 +00:00
return git ( ctx , workDir , append ( a , args ... ) ... )
2017-07-07 21:35:10 +00:00
}
2017-12-09 18:09:23 +00:00
func git ( ctx context . Context , dir string , args ... string ) ( * bytes . Buffer , error ) {
2017-12-16 07:17:06 +00:00
for {
2018-05-25 18:15:32 +00:00
stdout , stderr := logs . NewLogStreams ( ctx , false )
2017-12-16 07:17:06 +00:00
defer stdout . Close ( )
defer stderr . Close ( )
2018-04-11 00:22:27 +00:00
cmd := exec . Command ( "git" , args ... )
2017-12-16 07:17:06 +00:00
cmd . Dir = dir // some commands like submodule require this
buf := bytes . NewBuffer ( nil )
errbuf := bytes . NewBuffer ( nil )
cmd . Stdout = io . MultiWriter ( stdout , buf )
cmd . Stderr = io . MultiWriter ( stderr , errbuf )
2018-04-11 00:22:27 +00:00
// remote git commands spawn helper processes that inherit FDs and don't
// handle parent death signal so exec.CommandContext can't be used
err := runProcessGroup ( ctx , cmd )
2017-12-16 07:17:06 +00:00
if err != nil {
if strings . Contains ( errbuf . String ( ) , "--depth" ) || strings . Contains ( errbuf . String ( ) , "shallow" ) {
if newArgs := argsNoDepth ( args ) ; len ( args ) > len ( newArgs ) {
args = newArgs
continue
}
}
}
return buf , err
}
}
func argsNoDepth ( args [ ] string ) [ ] string {
out := make ( [ ] string , 0 , len ( args ) )
for _ , a := range args {
if a != "--depth=1" {
out = append ( out , a )
}
}
return out
2017-07-07 21:35:10 +00:00
}