snapshot/git/gitdb/checkout.go (147 lines of code) (raw):
package gitdb
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
"github.com/twitter/scoot/common/errors"
snap "github.com/twitter/scoot/snapshot"
"github.com/twitter/scoot/snapshot/git/repo"
)
// NOTE we assume in practice this only gets used where we are downloading and reading
// the file to a tempdir, and any underlying file/snapshot is deleted
func (db *DB) readFileAll(id snap.ID, path string) (string, error) {
v, err := db.parseID(id)
if err != nil {
return "", errors.NewError(err, errors.ReadFileAllFailureExitCode)
}
tmp, err := ioutil.TempDir(db.tmp, "readFileAll-")
if err != nil {
return "", errors.NewError(fmt.Errorf("Failed to create TempDir: %s", err), errors.ReadFileAllFailureExitCode)
}
defer os.RemoveAll(tmp)
r, err := v.DownloadTempRepo(db)
if err != nil {
return "", errors.NewError(err, errors.ReadFileAllFailureExitCode)
}
defer os.RemoveAll(r.Dir())
if v.Kind() != KindFSSnapshot {
return "", errors.NewError(fmt.Errorf("can only ReadFileAll from an FSSnapshot, but %v is a %v", id, v.Kind()), errors.ReadFileAllFailureExitCode)
}
s, err := r.Run("cat-file", "-p", fmt.Sprintf("%s:%s", v.SHA(), path))
if err != nil {
return "", errors.NewError(err, errors.ReadFileAllFailureExitCode)
}
return s, nil
}
// checkout creates a checkout of id.
func (db *DB) checkout(id snap.ID) (path string, err error) {
defer func() {
// If we're returning our repo dir, we need to keep the work tree locked, otherwise, we can unlock it.
// Note: we defer this to capture the various places 'path' is returned.
if path != db.dataRepo.Dir() {
db.workTreeLock.Unlock()
}
}()
v, err := db.parseID(id)
if err != nil {
return "", err
}
if err := v.Download(db); err != nil {
return "", err
}
switch v.Kind() {
case KindFSSnapshot:
// For FSSnapshots, we make a "bare checkout".
return db.checkoutFSSnapshot(v.SHA())
case KindGitCommitSnapshot:
return db.checkoutGitCommitSnapshot(v.SHA())
default:
return "", fmt.Errorf("cannot checkout value kind %v; id %v", v.Kind(), v.ID())
}
}
// checkoutFSSnapshot creates a new dir with a new index and checks out exactly that tree.
func (db *DB) checkoutFSSnapshot(sha string) (path string, err error) {
// we don't need the work tree
indexDir, err := ioutil.TempDir(db.tmp, "git-index")
if err != nil {
return "", err
}
indexFilename := filepath.Join(indexDir, "index")
defer os.RemoveAll(indexDir)
coDir, err := ioutil.TempDir("", "checkout")
if err != nil {
return "", err
}
extraEnv := []string{"GIT_INDEX_FILE=" + indexFilename, "GIT_WORK_TREE=" + coDir}
_, err = db.dataRepo.RunExtraEnv(extraEnv, "read-tree", sha)
if err != nil {
return "", err
}
_, err = db.dataRepo.RunExtraEnv(extraEnv, "checkout-index", "-a")
if err != nil {
return "", err
}
db.checkouts[coDir] = true
return coDir, nil
}
// checkoutGitCommitSnapshot checks out a commit into our work tree.
// We could use multiple work trees, except our internal git doesn't yet have work-tree support.
func (db *DB) checkoutGitCommitSnapshot(sha string) (path string, err error) {
// -d removes directories. -x ignores gitignore and removes everything.
// -f is force. -f the second time removes directories even if they're git repos themselves
cleanCmd := []string{"clean", "-f", "-f", "-d", "-x"}
if _, err := db.dataRepo.Run(cleanCmd...); err != nil {
return "", errors.NewError(fmt.Errorf("Unable to run git %v: %v", cleanCmd, err), errors.CleanFailureExitCode)
}
// -f overrides modified files
// -B resets or creates the named branch when checking out the given sha.
// Note: our worktree cannot be in detached head state after checkout since [Twitter] git needs a valid ref to fetch.
// we use scoot's tmp branch name so here subsequent fetch operations, ex: those in stream.go, can succeed.
checkoutCmd := []string{"checkout", "-fB", tempCheckoutBranch, sha}
if _, err := db.dataRepo.Run(checkoutCmd...); err != nil {
return "", errors.NewError(fmt.Errorf("Unable to run git %v: %v", checkoutCmd, err), errors.CheckoutFailureExitCode)
}
return db.dataRepo.Dir(), nil
}
func (db *DB) releaseCheckout(path string) error {
if path == db.dataRepo.Dir() {
db.workTreeLock.Unlock()
return nil
}
if exists := db.checkouts[path]; !exists {
return nil
}
err := os.RemoveAll(path)
if err == nil {
return nil
}
// TODO - this looks suspicious....
// why don't we delete the path entry in db.checkouts when we've successfully removed that dir (in if statement
// up at ln 152)?
delete(db.checkouts, path)
return errors.NewError(fmt.Errorf("Error:%v, Releasing checkout path: %v", err, path), errors.ReleaseCheckoutFailureCode)
}
func (db *DB) exportGitCommit(id snap.ID, externalRepo *repo.Repository) (string, error) {
v, err := db.parseID(id)
if err != nil {
return "", errors.NewError(err, errors.ExportGitCommitFailureExitCode)
}
if err := v.Download(db); err != nil {
return "", errors.NewError(err, errors.ExportGitCommitFailureExitCode)
}
if v.Kind() != KindGitCommitSnapshot {
return "", errors.NewError(fmt.Errorf("cannot export non-GitCommitSnapshot %v: %v", id, v.Kind()), errors.ExportGitCommitFailureExitCode)
}
if err := moveCommit(db.dataRepo, externalRepo, v.SHA()); err != nil {
return "", errors.NewError(err, errors.ExportGitCommitFailureExitCode)
}
return v.SHA(), nil
}
func moveCommit(from *repo.Repository, to *repo.Repository, sha string) error {
// Strategy: move a commit from 'from' to 'to'
// first, check if it's in 'to' (if so; skip)
// delete the ref in 'to'
// set the ref in 'from'.
// push from 'from' to 'to'.
// delete ref in both repos.
if from.Dir() == to.Dir() {
return nil
}
if _, err := to.Run("rev-parse", "--verify", fmt.Sprintf("%s^{commit}", sha)); err == nil {
return nil
}
log.Infof("Could not find commit=%s, continuing with moveCommit()", sha)
if _, err := to.Run("update-ref", "-d", tempRef); err != nil {
return err
}
if _, err := from.Run("update-ref", tempRef, sha); err != nil {
return err
}
if _, err := from.Run("push", "-f", to.Dir(), tempRef); err != nil {
return err
}
if _, err := from.Run("update-ref", "-d", tempRef); err != nil {
return err
}
if _, err := to.Run("update-ref", "-d", tempRef); err != nil {
return err
}
return nil
}