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

@ -32,11 +32,18 @@ var (
)
const (
workDiskGitConfigRelativePath = ".gitconfig"
workDiskOpencodeAuthDirRelativePath = ".local/share/opencode"
workDiskOpencodeAuthRelativePath = workDiskOpencodeAuthDirRelativePath + "/auth.json"
hostGlobalGitIdentitySource = "git config --global"
hostOpencodeAuthDefaultDisplayPath = "~/" + workDiskOpencodeAuthRelativePath
)
type gitIdentity struct {
Name string
Email string
}
func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm model.VMRecord, err error) {
d.mu.Lock()
defer d.mu.Unlock()
@ -933,6 +940,32 @@ func (d *Daemon) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm *model.VM
return nil
}
func (d *Daemon) ensureGitIdentityOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
runner := d.runner
if runner == nil {
runner = system.NewRunner()
}
identity, err := resolveHostGlobalGitIdentity(ctx, runner)
if err != nil {
d.warnGitIdentitySyncSkipped(*vm, hostGlobalGitIdentitySource, err)
return nil
}
vmCreateStage(ctx, "prepare_work_disk", "syncing git identity")
workMount, cleanupWork, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
if err != nil {
return err
}
defer cleanupWork()
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
return err
}
return writeGitIdentity(ctx, runner, filepath.Join(workMount, workDiskGitConfigRelativePath), identity)
}
func (d *Daemon) ensureOpencodeAuthOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
hostAuthPath, err := resolveHostOpencodeAuthPath()
if err != nil {
@ -990,6 +1023,69 @@ func resolveHostOpencodeAuthPath() (string, error) {
return filepath.Join(home, workDiskOpencodeAuthRelativePath), nil
}
func resolveHostGlobalGitIdentity(ctx context.Context, runner system.CommandRunner) (gitIdentity, error) {
name, err := gitConfigValue(ctx, runner, nil, "user.name")
if err != nil {
return gitIdentity{}, err
}
if name == "" {
return gitIdentity{}, errors.New("host git user.name is empty")
}
email, err := gitConfigValue(ctx, runner, nil, "user.email")
if err != nil {
return gitIdentity{}, err
}
if email == "" {
return gitIdentity{}, errors.New("host git user.email is empty")
}
return gitIdentity{Name: name, Email: email}, nil
}
func gitConfigValue(ctx context.Context, runner system.CommandRunner, extraArgs []string, key string) (string, error) {
args := []string{"config"}
args = append(args, extraArgs...)
args = append(args, "--default", "", "--get", key)
out, err := runner.Run(ctx, "git", args...)
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
func writeGitIdentity(ctx context.Context, runner system.CommandRunner, gitConfigPath string, identity gitIdentity) error {
existing, err := runner.RunSudo(ctx, "cat", gitConfigPath)
if err != nil {
existing = nil
}
tmpFile, err := os.CreateTemp("", "banger-gitconfig-*")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
if _, err := tmpFile.Write(existing); err != nil {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
return err
}
if err := tmpFile.Close(); err != nil {
_ = os.Remove(tmpPath)
return err
}
defer os.Remove(tmpPath)
if _, err := runner.Run(ctx, "git", "config", "--file", tmpPath, "user.name", identity.Name); err != nil {
return err
}
if _, err := runner.Run(ctx, "git", "config", "--file", tmpPath, "user.email", identity.Email); err != nil {
return err
}
_, err = runner.RunSudo(ctx, "install", "-m", "644", tmpPath, gitConfigPath)
return err
}
func (d *Daemon) warnOpencodeAuthSyncSkipped(vm model.VMRecord, hostPath string, err error) {
if d.logger == nil || err == nil {
return
@ -997,6 +1093,13 @@ func (d *Daemon) warnOpencodeAuthSyncSkipped(vm model.VMRecord, hostPath string,
d.logger.Warn("guest opencode auth sync skipped", append(vmLogAttrs(vm), "host_path", hostPath, "error", err.Error())...)
}
func (d *Daemon) warnGitIdentitySyncSkipped(vm model.VMRecord, source string, err error) {
if d.logger == nil || err == nil {
return
}
d.logger.Warn("guest git identity sync skipped", append(vmLogAttrs(vm), "source", source, "error", err.Error())...)
}
func mergeAuthorizedKey(existing, managed []byte) []byte {
managedLine := strings.TrimSpace(string(managed))
if managedLine == "" {