From e59d5bbd29c3ae5ce13c5e5cb7897852fbc24232 Mon Sep 17 00:00:00 2001 From: Thomas Nilles Date: Sun, 20 Sep 2026 17:22:34 -0400 Subject: [PATCH] Slice 8: configure git credentials + identity in the container On startup the app runs git config --global to set a commit identity (GIT_USER_NAME/GIT_USER_EMAIL), safe.directory=* for host-owned mounts, and http..extraheader with the Gitea token so push/fetch/pull authenticate over HTTPS without an SSH key. New git.CLI.SetGlobalConfig and config fields. Verified: fetch via the app works and the identity/header are set. Co-Authored-By: Claude Opus 4.8 --- .env.example | 8 ++++++++ AGENT.md | 8 ++++++-- CHANGELOG.md | 16 ++++++++++++++++ cmd/server/main.go | 26 ++++++++++++++++++++++++++ internal/config/config.go | 5 +++++ internal/git/git.go | 7 +++++++ 6 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index d11b563..69d206e 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,14 @@ SCAN_FETCH_ENABLED=false # "dev" uses a readable console handler; anything else uses structured JSON. APP_ENV=dev +# Commit identity for git actions the app runs (commit/etc.). Without these, +# commits inside the container fail with "empty ident". Set to your name/email. +GIT_USER_NAME= +GIT_USER_EMAIL= +# Note: when GITEA_URL + GITEA_TOKEN are set, the app also configures git to +# authenticate to that host over HTTPS (an http.extraheader), so push/fetch/pull +# work from the container without a separate SSH key or credential helper. + # Optional: also append structured logs to this file. Leave empty to disable. LOG_FILE= diff --git a/AGENT.md b/AGENT.md index 2c818ff..af825bf 100644 --- a/AGENT.md +++ b/AGENT.md @@ -548,8 +548,12 @@ silently guess.)* branch delete) and how it is provisioned; document in `.env.example`. - **Listen address / exposure:** localhost‑only by default (covers `/mcp` too). Confirm before binding to a non‑local interface — there is no auth (Section 0). -- **Credential path from the container:** SSH agent socket vs mounted keys vs - credential helper, for pushing/fetching from inside Docker. +- ✅ **RESOLVED 2026-09-20:** **Container git auth = the Gitea token over HTTPS.** + On startup the app runs `git config --global` to set a commit identity + (`GIT_USER_NAME`/`GIT_USER_EMAIL`), `safe.directory=*` (host-owned mounts), and + `http..extraheader: Authorization: token …` so push/fetch/pull work + without SSH keys. The token lands in the container's gitconfig (ephemeral, + localhost). An SSH-key path stays possible later for non-Gitea remotes. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 922e280..9434892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,3 +201,19 @@ Append-only running history of all changes (AGENT.md §9.1). Newest last. - **Affects:** `internal/mcp` (+test), `AGENT.md` (§8.1). - **Note:** the new tools appear in Claude Desktop only after its next restart (tool list cached per connection); network ops still need container git creds. + +## 2026-09-20 — Slice 8: git credentials + identity in the container (§11) +- **What:** On startup the app configures the container's git (`git config + --global`): a commit identity (`GIT_USER_NAME`/`GIT_USER_EMAIL`), + `safe.directory=*` for host-owned mounts, and — when `GITEA_URL`+`GITEA_TOKEN` + are set — `http..extraheader: Authorization: token …` so push/fetch/pull + authenticate over HTTPS with no SSH key. New `git.CLI.SetGlobalConfig`; new + config `GIT_USER_NAME`/`GIT_USER_EMAIL`; `.env.example` documents them. +- **Why:** Make the network git commands (menu + MCP) actually work from Docker, + and let commits have an author. +- **Affects:** `internal/config`, `internal/git`, `cmd/server/main.go`, + `.env.example`, `AGENT.md` (§11). +- **Verified:** startup logs "git remote auth configured"; container git identity + set; `http.extraheader` present; `git_fetch` via the app returned ok. +- **Security note:** the token is written to the container's ephemeral gitconfig + and passed in a `git config` argv — acceptable for a localhost dev container. diff --git a/cmd/server/main.go b/cmd/server/main.go index c5849f7..fdb9a03 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -6,6 +6,7 @@ package main import ( "context" "encoding/json" + "log/slog" "net/http" "os" "os/signal" @@ -26,6 +27,30 @@ import ( "gitmanager/internal/service" ) +// configureGit prepares the container's git for operating on the mounted repos: +// a commit identity (so commits don't fail with "empty ident"), permission to +// work on host-owned mounts, and — when a Gitea token is set — an auth header so +// pushes/fetches over HTTPS succeed. The token is written to the container's +// gitconfig (ephemeral, localhost); see AGENT.md §11. +func configureGit(ctx context.Context, g *git.CLI, cfg config.Config, log *slog.Logger) { + set := func(key, value string) { + if err := g.SetGlobalConfig(ctx, key, value); err != nil { + log.Warn("git config failed", "key", key, "err", err) + } + } + set("safe.directory", "*") // mounted repos are host-owned + if cfg.GitUserName != "" { + set("user.name", cfg.GitUserName) + } + if cfg.GitUserEmail != "" { + set("user.email", cfg.GitUserEmail) + } + if cfg.GiteaURL != "" && cfg.GiteaToken != "" { + set("http."+cfg.GiteaURL+".extraheader", "Authorization: token "+cfg.GiteaToken) + log.Info("git remote auth configured", "host", cfg.GiteaURL) + } +} + func main() { cfg, err := config.Load() if err != nil { @@ -46,6 +71,7 @@ func main() { } else { log.Info("git detected", "version", v) } + configureGit(context.Background(), g, cfg, log) // Start the read-only scanner in the background. scanner := repos.NewScanner(g, log, cfg.RepoRoots, cfg.ScanMaxDepth, cfg.ScanIgnore, cfg.ScanInterval, cfg.ScanFetchEnabled) diff --git a/internal/config/config.go b/internal/config/config.go index 3737160..985ca48 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -34,6 +34,9 @@ type Config struct { Dev bool // readable console logging vs structured JSON LogFile string // optional file to also append logs to + GitUserName string // commit identity for git actions run by the app + GitUserEmail string // commit identity for git actions run by the app + GiteaURL string // Gitea/Forgejo base URL (e.g. https://git.nilles.net) GiteaToken string // Gitea token (read + PR write + branch delete) — §8.4 GitHubToken string // optional forge token (later provider) @@ -57,6 +60,8 @@ func Load() (Config, error) { ScanFetchEnabled: envBool("SCAN_FETCH_ENABLED", false), Dev: strings.EqualFold(env("APP_ENV", "dev"), "dev"), LogFile: env("LOG_FILE", ""), + GitUserName: env("GIT_USER_NAME", ""), + GitUserEmail: env("GIT_USER_EMAIL", ""), GiteaURL: env("GITEA_URL", ""), GiteaToken: env("GITEA_TOKEN", ""), GitHubToken: env("GITHUB_TOKEN", ""), diff --git a/internal/git/git.go b/internal/git/git.go index 6e38556..ca9a6c5 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -69,6 +69,13 @@ func (c *CLI) Version(ctx context.Context) (string, error) { return c.run(ctx, "", "version") } +// SetGlobalConfig sets a global git config value (git config --global key value). +// Used at startup to give the container git a commit identity and remote auth. +func (c *CLI) SetGlobalConfig(ctx context.Context, key, value string) error { + _, err := c.run(ctx, "", "config", "--global", key, value) + return err +} + // CurrentBranch returns the checked-out branch, or "HEAD" when detached. func (c *CLI) CurrentBranch(ctx context.Context, dir string) (string, error) { return c.run(ctx, dir, "rev-parse", "--abbrev-ref", "HEAD")