e86b6655a3
* fix(worktree): handle submodules in worktree creation Repos with git submodules failed during worktree creation when git-crypt was also present. The `git checkout HEAD -- .` command in unlockGitCryptAndCheckout() tried to process submodule gitlink entries, but git resolved their git dirs relative to the worktree-specific git dir instead of the common dir, causing "not a git repository" errors. - Detect submodule paths via `git ls-tree -r HEAD` (mode 160000) - Exclude submodule paths from git-crypt checkout using :(exclude) pathspecs - Initialize submodules after worktree creation via `git submodule update --init` (non-fatal — unreachable URLs are logged as warnings) - Add submodule init to all worktree creation paths (git-crypt and non-git-crypt) * fix(worktree): address review — recursive submodule init, consistent call sites - Add --recursive to git submodule update --init for nested submodules - Call initSubmodules unconditionally in git-crypt path (matching manager.go) * fix(worktree): restore submodule index entries after git-crypt checkout The :(exclude) pathspec used to skip submodules during git checkout also prevents their gitlink entries from being written to the index. This leaves git status showing them as staged deletions. After checkout, run git reset HEAD for each excluded submodule path to copy the gitlink entry from HEAD back into the index. * test(worktree): add git status assertion to submodule unlock test Validate that the index reset after :(exclude) checkout leaves no staged deletions for submodule paths. * fix(worktree): use literal pathspec for submodule exclusion Use :(literal,exclude) instead of :(exclude) to prevent glob metacharacters in submodule paths from being interpreted as patterns. * fix(worktree): fix initSubmodules docstring to match --recursive flag
54 lines
1.6 KiB
Go
54 lines
1.6 KiB
Go
package worktree
|
|
|
|
import (
|
|
"context"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// getSubmodulePaths returns the paths of all submodules registered in HEAD.
|
|
// It reads from the git object store (git ls-tree), so it works in --no-checkout worktrees.
|
|
// Returns nil (not an error) if there are no submodules.
|
|
func getSubmodulePaths(ctx context.Context, dir string) ([]string, error) {
|
|
cmd := exec.CommandContext(ctx, "git", "ls-tree", "-r", "HEAD")
|
|
cmd.Dir = dir
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var paths []string
|
|
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
// Format: "<mode> <type> <hash>\t<path>"
|
|
// Submodules have mode 160000 and type "commit".
|
|
if strings.HasPrefix(line, "160000 ") {
|
|
if _, path, ok := strings.Cut(line, "\t"); ok {
|
|
paths = append(paths, path)
|
|
}
|
|
}
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
// initSubmodules runs "git submodule update --init --recursive" in the given directory.
|
|
// Failures are non-fatal: submodule URLs may be unreachable (private repos,
|
|
// missing credentials), but the worktree is still usable for non-submodule files.
|
|
func (m *Manager) initSubmodules(ctx context.Context, dir string) {
|
|
cmd := exec.CommandContext(ctx, "git", "submodule", "update", "--init", "--recursive")
|
|
cmd.Dir = dir
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
m.logger.Warn("git submodule update --init failed (non-fatal)",
|
|
zap.String("dir", dir),
|
|
zap.String("output", string(output)),
|
|
zap.Error(err))
|
|
return
|
|
}
|
|
m.logger.Debug("initialized submodules in worktree", zap.String("dir", dir))
|
|
}
|