in iterator/git.go [225:441]
func (obj *Git) Recurse(ctx context.Context, scan interfaces.ScanFunc) ([]interfaces.Iterator, error) {
relDir := safepath.UnsafeParseIntoRelDir("git/")
prefix := safepath.JoinToAbsDir(obj.Prefix, relDir)
if err := os.MkdirAll(prefix.Path(), interfaces.Umask); err != nil {
return nil, err
}
// make a unique ID for the directory
// XXX: we can consider different algorithms or methods here later...
sum := sha256.Sum256([]byte(obj.URL))
hashRelDir, err := safepath.ParseIntoRelDir(fmt.Sprintf("%x", sum))
if err != nil {
return nil, err
}
repoAbsDir := safepath.JoinToAbsDir(prefix, hashRelDir)
mapMutex.Lock()
mu, exists := mutexes[obj.URL]
if !exists {
mu = &sync.Mutex{}
mutexes[obj.URL] = mu
}
mapMutex.Unlock()
if obj.Debug {
obj.Logf("locking: %s", obj.String())
}
mu.Lock() // locking happens here (unlock on all errors/returns!)
once := &sync.Once{}
obj.unlock = func() {
fn := func() {
if obj.Debug {
obj.Logf("unlocking: %s", obj.String())
}
mu.Unlock()
}
once.Do(fn)
}
// XXX: unlock when context closes?
obj.Logf("cloning %s into %s", obj.String(), repoAbsDir)
directory := repoAbsDir.Path()
isBare := false
repository, err := git.PlainCloneContext(ctx, directory, isBare, &git.CloneOptions{
URL: obj.URL,
// Don't recurse, we do it manually with the FsIterator, as this
// way we'll get all the repositories cloned next to each other,
// instead of in a big recursive filesystem tree.
RecurseSubmodules: git.NoRecurseSubmodules,
//Auth transport.AuthMethod
//Progress: os.Stdout,
})
if err == git.ErrRepositoryAlreadyExists {
obj.Logf("repo %s already exists", obj.String())
repository, err = git.PlainOpenWithOptions(directory, &git.PlainOpenOptions{})
if err != nil {
obj.unlock()
return nil, errwrap.Wrapf(err, "error opening repository")
}
} else if err != nil {
obj.unlock()
return nil, errwrap.Wrapf(err, "error cloning repository")
}
var hash plumbing.Hash
if obj.Hash != "" {
hash = plumbing.NewHash(obj.Hash)
}
if obj.Ref != "" {
// TODO: update with https://github.com/go-git/go-git/issues/289
var err error
ref := plumbing.ReferenceName(obj.Ref)
if hash, err = getCommitFromRef(repository, ref); err != nil {
obj.unlock()
return nil, err
}
}
if obj.Rev != "" {
// This API returns a *Hash. Everything else uses a Hash as-is.
pHash, err := repository.ResolveRevision(plumbing.Revision(obj.Rev))
if err != nil {
obj.unlock()
return nil, err
}
hash = *pHash
}
if hash.IsZero() { // desired state not specified, use HEAD branch...
// XXX: I'm not sure this is the correct way to find the HEAD.
// XXX: eg: https://github.com/ansible/ansible uses `devel`.
// XXX: running: `git remote show origin`, shows: `HEAD branch: <branch>`
// XXX: how do I extract that without looking in: ~/.git/refs/remotes/origin/*
names := []string{"HEAD", "master", "main"}
var err error
var name string
for _, name = range names {
ref := plumbing.NewRemoteReferenceName("origin", name)
// git symbolic-ref refs/remotes/origin/HEAD ?
hash, err = getCommitFromRef(repository, ref)
if err == plumbing.ErrReferenceNotFound {
continue // this error means we continue
}
break // if a different error or a nil, we are done!
}
if err != nil { // check here if we found something!
obj.unlock()
return nil, errwrap.Wrapf(err, "could not find default HEAD in origin")
}
obj.Logf("default HEAD is at: %s", name)
}
head, err := repository.Head()
if err != nil {
obj.unlock()
return nil, err
}
if obj.Debug {
obj.Logf("HEAD is at: %s", head.Hash())
}
// If the desired state, is not equal to the actual state, set it!
if hash.String() != head.Hash().String() {
worktree, err := repository.Worktree()
if err != nil {
obj.unlock()
return nil, err
}
checkoutOptions := &git.CheckoutOptions{}
if obj.Hash != "" {
checkoutOptions.Hash = hash
}
// We use the consistent hash approach to identify the repo so
// that we have a unique identifier to use everywhere...
//if obj.Ref != "" {
// checkoutOptions.Branch = plumbing.ReferenceName(obj.Ref)
//}
if err := worktree.Checkout(checkoutOptions); err != nil {
obj.unlock()
return nil, err
}
}
obj.iterators = []interfaces.Iterator{}
u, err := url.Parse(obj.URL) // build a url to modify
if err != nil {
obj.unlock()
return nil, err
}
u.Scheme = GitSchemeRaw
u.Opaque = "" // encoded opaque data
if _, has := u.User.Password(); has { // redact password
u.User = url.UserPassword(u.User.Username(), "")
}
//u.Host = ? // host or host:port
// remove the .git suffix of old-style repos
if obj.TrimGitSuffix && strings.HasSuffix(u.Path, ".git") {
u.Path = strings.TrimSuffix(u.Path, ".git")
}
u.RawPath = "" // encoded path hint (see EscapedPath method)
u.ForceQuery = false // append a query ('?') even if RawQuery is empty
v := url.Values{}
v.Set("sha1", hash.String())
u.RawQuery = v.Encode() // encoded query values, without '?'
u.Fragment = "" // fragment for references, without '#'
u.RawFragment = "" // encoded fragment hint (see EscapedFragment method)
iterator := &Fs{
Debug: obj.Debug,
Logf: func(format string, v ...interface{}) {
obj.Logf(format, v...) // TODO: add a prefix?
},
Prefix: obj.Prefix,
Iterator: obj,
Path: repoAbsDir,
GenUID: func(safePath safepath.Path) (string, error) {
if !safepath.HasPrefix(safePath, repoAbsDir) {
// programming error
return "", fmt.Errorf("path doesn't have prefix")
}
p := ""
// remove repoAbsDir prefix from safePath to get a relPath
relPath, err := safepath.StripPrefix(safePath, repoAbsDir)
if err == nil {
p = relPath.String()
} else if err != nil && safePath.String() != repoAbsDir.String() {
// programming error
return "", errwrap.Wrapf(err, "problem stripping prefix")
}
x := *u // copy
x.Path += "/" + p
return x.String(), nil
},
//Unlock: unlock,
}
obj.iterators = append(obj.iterators, iterator)
return obj.iterators, nil
}