fix: isolate git clone operations from parent repos

Clone operations now use a temp directory to completely isolate from
any git repository at the process cwd. This fixes the issue where
`.git` directory was deleted during `gt rig add` when the town root
was initialized with `--git`.

The fix applies to all four clone functions:
- Clone()
- CloneBare()
- CloneWithReference()
- CloneBareWithReference()

Each function now:
1. Creates a temp directory
2. Runs clone inside temp dir with cmd.Dir set
3. Sets GIT_CEILING_DIRECTORIES to prevent parent repo discovery
4. Moves the cloned repo to the final destination
This commit is contained in:
Dustin Smith
2026-01-19 22:34:04 +07:00
committed by John Ogle
parent 0953863641
commit eb8024c1f9

View File

@@ -116,13 +116,35 @@ func (g *Git) wrapError(err error, stdout, stderr string, args []string) error {
// Clone clones a repository to the destination.
func (g *Git) Clone(url, dest string) error {
cmd := exec.Command("git", "clone", url, dest)
// Ensure destination directory's parent exists
destParent := filepath.Dir(dest)
if err := os.MkdirAll(destParent, 0755); err != nil {
return fmt.Errorf("creating destination parent: %w", err)
}
// Run clone from a temporary directory to completely isolate from any
// git repo at the process cwd. Then move the result to the destination.
tmpDir, err := os.MkdirTemp("", "gt-clone-*")
if err != nil {
return fmt.Errorf("creating temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
tmpDest := filepath.Join(tmpDir, filepath.Base(dest))
cmd := exec.Command("git", "clone", url, tmpDest)
cmd.Dir = tmpDir
cmd.Env = append(os.Environ(), "GIT_CEILING_DIRECTORIES="+tmpDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return g.wrapError(err, stdout.String(), stderr.String(), []string{"clone", url})
}
// Move to final destination
if err := os.Rename(tmpDest, dest); err != nil {
return fmt.Errorf("moving clone to destination: %w", err)
}
// Configure hooks path for Gas Town clones
if err := configureHooksPath(dest); err != nil {
return err
@@ -134,13 +156,35 @@ func (g *Git) Clone(url, dest string) error {
// CloneWithReference clones a repository using a local repo as an object reference.
// This saves disk by sharing objects without changing remotes.
func (g *Git) CloneWithReference(url, dest, reference string) error {
cmd := exec.Command("git", "clone", "--reference-if-able", reference, url, dest)
// Ensure destination directory's parent exists
destParent := filepath.Dir(dest)
if err := os.MkdirAll(destParent, 0755); err != nil {
return fmt.Errorf("creating destination parent: %w", err)
}
// Run clone from a temporary directory to completely isolate from any
// git repo at the process cwd. Then move the result to the destination.
tmpDir, err := os.MkdirTemp("", "gt-clone-*")
if err != nil {
return fmt.Errorf("creating temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
tmpDest := filepath.Join(tmpDir, filepath.Base(dest))
cmd := exec.Command("git", "clone", "--reference-if-able", reference, url, tmpDest)
cmd.Dir = tmpDir
cmd.Env = append(os.Environ(), "GIT_CEILING_DIRECTORIES="+tmpDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return g.wrapError(err, stdout.String(), stderr.String(), []string{"clone", "--reference-if-able", url})
}
// Move to final destination
if err := os.Rename(tmpDest, dest); err != nil {
return fmt.Errorf("moving clone to destination: %w", err)
}
// Configure hooks path for Gas Town clones
if err := configureHooksPath(dest); err != nil {
return err
@@ -152,13 +196,35 @@ func (g *Git) CloneWithReference(url, dest, reference string) error {
// CloneBare clones a repository as a bare repo (no working directory).
// This is used for the shared repo architecture where all worktrees share a single git database.
func (g *Git) CloneBare(url, dest string) error {
cmd := exec.Command("git", "clone", "--bare", url, dest)
// Ensure destination directory's parent exists
destParent := filepath.Dir(dest)
if err := os.MkdirAll(destParent, 0755); err != nil {
return fmt.Errorf("creating destination parent: %w", err)
}
// Run clone from a temporary directory to completely isolate from any
// git repo at the process cwd. Then move the result to the destination.
tmpDir, err := os.MkdirTemp("", "gt-clone-*")
if err != nil {
return fmt.Errorf("creating temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
tmpDest := filepath.Join(tmpDir, filepath.Base(dest))
cmd := exec.Command("git", "clone", "--bare", url, tmpDest)
cmd.Dir = tmpDir
cmd.Env = append(os.Environ(), "GIT_CEILING_DIRECTORIES="+tmpDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return g.wrapError(err, stdout.String(), stderr.String(), []string{"clone", "--bare", url})
}
// Move to final destination
if err := os.Rename(tmpDest, dest); err != nil {
return fmt.Errorf("moving clone to destination: %w", err)
}
// Configure refspec so worktrees can fetch and see origin/* refs
return configureRefspec(dest)
}
@@ -212,13 +278,35 @@ func configureRefspec(repoPath string) error {
// CloneBareWithReference clones a bare repository using a local repo as an object reference.
func (g *Git) CloneBareWithReference(url, dest, reference string) error {
cmd := exec.Command("git", "clone", "--bare", "--reference-if-able", reference, url, dest)
// Ensure destination directory's parent exists
destParent := filepath.Dir(dest)
if err := os.MkdirAll(destParent, 0755); err != nil {
return fmt.Errorf("creating destination parent: %w", err)
}
// Run clone from a temporary directory to completely isolate from any
// git repo at the process cwd. Then move the result to the destination.
tmpDir, err := os.MkdirTemp("", "gt-clone-*")
if err != nil {
return fmt.Errorf("creating temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
tmpDest := filepath.Join(tmpDir, filepath.Base(dest))
cmd := exec.Command("git", "clone", "--bare", "--reference-if-able", reference, url, tmpDest)
cmd.Dir = tmpDir
cmd.Env = append(os.Environ(), "GIT_CEILING_DIRECTORIES="+tmpDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return g.wrapError(err, stdout.String(), stderr.String(), []string{"clone", "--bare", "--reference-if-able", url})
}
// Move to final destination
if err := os.Rename(tmpDest, dest); err != nil {
return fmt.Errorf("moving clone to destination: %w", err)
}
// Configure refspec so worktrees can fetch and see origin/* refs
return configureRefspec(dest)
}