Sync Git identity into guest VMs

Populate guest /root/.gitconfig from host git config --global during work-disk preparation so plain VM shells can commit.

Resolve user.name and user.email from the source repo for vm run and write them only into the imported checkout, preserving repo-specific identity overrides.

Update mounted guest .gitconfig through a host temp file plus sudo install instead of direct git config --file writes, since the mounted root-owned work disk blocks Git lockfile creation.

Validated with GOCACHE=/tmp/banger-gocache go test ./..., make build, and a live alpine vm create smoke check for guest git config.
This commit is contained in:
Thales Maciel 2026-03-22 19:30:36 -03:00
parent f798e1db33
commit 42b4a18c63
No known key found for this signature in database
GPG key ID: 33112E6833C34679
5 changed files with 308 additions and 0 deletions

View file

@ -111,6 +111,8 @@ type vmRunRepoSpec struct {
CurrentBranch string
BranchName string
BaseCommit string
GitUserName string
GitUserEmail string
OverlayPaths []string
}
@ -1444,6 +1446,15 @@ func inspectVMRunRepo(ctx context.Context, rawPath, branchName, fromRef string)
}
}
gitUserName, err := gitResolvedConfigValue(ctx, repoRoot, "user.name")
if err != nil {
return vmRunRepoSpec{}, fmt.Errorf("resolve git user.name for %s: %w", repoRoot, err)
}
gitUserEmail, err := gitResolvedConfigValue(ctx, repoRoot, "user.email")
if err != nil {
return vmRunRepoSpec{}, fmt.Errorf("resolve git user.email for %s: %w", repoRoot, err)
}
overlayPaths, err := listVMRunOverlayPaths(ctx, repoRoot)
if err != nil {
return vmRunRepoSpec{}, err
@ -1457,6 +1468,8 @@ func inspectVMRunRepo(ctx context.Context, rawPath, branchName, fromRef string)
CurrentBranch: currentBranch,
BranchName: branchName,
BaseCommit: baseCommit,
GitUserName: gitUserName,
GitUserEmail: gitUserEmail,
OverlayPaths: overlayPaths,
}, nil
}
@ -1552,6 +1565,10 @@ func gitTrimmedOutput(ctx context.Context, dir string, args ...string) (string,
return strings.TrimSpace(string(output)), nil
}
func gitResolvedConfigValue(ctx context.Context, dir, key string) (string, error) {
return gitTrimmedOutput(ctx, dir, "config", "--default", "", "--get", key)
}
func parseNullSeparatedOutput(output []byte) []string {
chunks := bytes.Split(output, []byte{0})
values := make([]string, 0, len(chunks))
@ -1658,6 +1675,10 @@ func vmRunCloneScript(spec vmRunRepoSpec) string {
}
script.WriteString("find \"$DIR\" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +\n")
script.WriteString("git config --global --add safe.directory \"$DIR\"\n")
if strings.TrimSpace(spec.GitUserName) != "" && strings.TrimSpace(spec.GitUserEmail) != "" {
fmt.Fprintf(&script, "git -C \"$DIR\" config user.name %s\n", shellQuote(spec.GitUserName))
fmt.Fprintf(&script, "git -C \"$DIR\" config user.email %s\n", shellQuote(spec.GitUserEmail))
}
return script.String()
}

View file

@ -918,6 +918,10 @@ func TestInspectVMRunRepoUsesRepoRootAndOverlayPaths(t *testing.T) {
}
repoRoot := t.TempDir()
globalConfigPath := filepath.Join(t.TempDir(), "global.gitconfig")
t.Setenv("GIT_CONFIG_GLOBAL", globalConfigPath)
testRunGit(t, repoRoot, "config", "--global", "user.email", "global@example.com")
testRunGit(t, repoRoot, "config", "--global", "user.name", "Global User")
testRunGit(t, repoRoot, "init")
testRunGit(t, repoRoot, "config", "user.email", "test@example.com")
testRunGit(t, repoRoot, "config", "user.name", "Banger Test")
@ -968,6 +972,12 @@ func TestInspectVMRunRepoUsesRepoRootAndOverlayPaths(t *testing.T) {
if spec.BaseCommit != spec.HeadCommit {
t.Fatalf("BaseCommit = %q, want head %q", spec.BaseCommit, spec.HeadCommit)
}
if spec.GitUserName != "Banger Test" {
t.Fatalf("GitUserName = %q, want Banger Test", spec.GitUserName)
}
if spec.GitUserEmail != "test@example.com" {
t.Fatalf("GitUserEmail = %q, want test@example.com", spec.GitUserEmail)
}
wantOverlay := []string{".gitignore", "dir/keep.txt", "tracked.txt", "untracked.txt"}
if !reflect.DeepEqual(spec.OverlayPaths, wantOverlay) {
t.Fatalf("OverlayPaths = %v, want %v", spec.OverlayPaths, wantOverlay)
@ -1100,6 +1110,8 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
CurrentBranch: "main",
BranchName: "feature",
BaseCommit: "cafebabe",
GitUserName: "Repo User",
GitUserEmail: "repo@example.com",
OverlayPaths: []string{"tracked.txt", "nested/keep.txt"},
}
err := runVMRun(
@ -1149,6 +1161,12 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
if !strings.Contains(fakeClient.script, `git config --global --add safe.directory "$DIR"`) {
t.Fatalf("script = %q, want guest safe.directory config", fakeClient.script)
}
if !strings.Contains(fakeClient.script, `git -C "$DIR" config user.name 'Repo User'`) {
t.Fatalf("script = %q, want guest repo user.name config", fakeClient.script)
}
if !strings.Contains(fakeClient.script, `git -C "$DIR" config user.email 'repo@example.com'`) {
t.Fatalf("script = %q, want guest repo user.email config", fakeClient.script)
}
if fakeClient.streamSourceDir != repoRoot {
t.Fatalf("streamSourceDir = %q, want %q", fakeClient.streamSourceDir, repoRoot)
}
@ -1167,6 +1185,19 @@ func TestRunVMRunCreatesImportsAndAttaches(t *testing.T) {
}
}
func TestVMRunCloneScriptSkipsRepoGitIdentityWhenIncomplete(t *testing.T) {
script := vmRunCloneScript(vmRunRepoSpec{
RepoName: "repo",
HeadCommit: "deadbeef",
CurrentBranch: "main",
GitUserName: "Repo User",
})
if strings.Contains(script, `git -C "$DIR" config user.name`) || strings.Contains(script, `git -C "$DIR" config user.email`) {
t.Fatalf("script = %q, want no repo-local git identity commands", script)
}
}
func TestNewBangerdCommandRejectsArgs(t *testing.T) {
cmd := NewBangerdCommand()
cmd.SetArgs([]string{"extra"})