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.<GITEA_URL>.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 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,14 @@ SCAN_FETCH_ENABLED=false
|
|||||||
# "dev" uses a readable console handler; anything else uses structured JSON.
|
# "dev" uses a readable console handler; anything else uses structured JSON.
|
||||||
APP_ENV=dev
|
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.
|
# Optional: also append structured logs to this file. Leave empty to disable.
|
||||||
LOG_FILE=
|
LOG_FILE=
|
||||||
|
|
||||||
|
|||||||
@@ -548,8 +548,12 @@ silently guess.)*
|
|||||||
branch delete) and how it is provisioned; document in `.env.example`.
|
branch delete) and how it is provisioned; document in `.env.example`.
|
||||||
- **Listen address / exposure:** localhost‑only by default (covers `/mcp` too).
|
- **Listen address / exposure:** localhost‑only by default (covers `/mcp` too).
|
||||||
Confirm before binding to a non‑local interface — there is no auth (Section 0).
|
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
|
- ✅ **RESOLVED 2026-09-20:** **Container git auth = the Gitea token over HTTPS.**
|
||||||
credential helper, for pushing/fetching from inside Docker.
|
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.<GITEA_URL>.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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
- **Affects:** `internal/mcp` (+test), `AGENT.md` (§8.1).
|
||||||
- **Note:** the new tools appear in Claude Desktop only after its next restart
|
- **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.
|
(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.<url>.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.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -26,6 +27,30 @@ import (
|
|||||||
"gitmanager/internal/service"
|
"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() {
|
func main() {
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -46,6 +71,7 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
log.Info("git detected", "version", v)
|
log.Info("git detected", "version", v)
|
||||||
}
|
}
|
||||||
|
configureGit(context.Background(), g, cfg, log)
|
||||||
|
|
||||||
// Start the read-only scanner in the background.
|
// Start the read-only scanner in the background.
|
||||||
scanner := repos.NewScanner(g, log, cfg.RepoRoots, cfg.ScanMaxDepth, cfg.ScanIgnore, cfg.ScanInterval, cfg.ScanFetchEnabled)
|
scanner := repos.NewScanner(g, log, cfg.RepoRoots, cfg.ScanMaxDepth, cfg.ScanIgnore, cfg.ScanInterval, cfg.ScanFetchEnabled)
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ type Config struct {
|
|||||||
Dev bool // readable console logging vs structured JSON
|
Dev bool // readable console logging vs structured JSON
|
||||||
LogFile string // optional file to also append logs to
|
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)
|
GiteaURL string // Gitea/Forgejo base URL (e.g. https://git.nilles.net)
|
||||||
GiteaToken string // Gitea token (read + PR write + branch delete) — §8.4
|
GiteaToken string // Gitea token (read + PR write + branch delete) — §8.4
|
||||||
GitHubToken string // optional forge token (later provider)
|
GitHubToken string // optional forge token (later provider)
|
||||||
@@ -57,6 +60,8 @@ func Load() (Config, error) {
|
|||||||
ScanFetchEnabled: envBool("SCAN_FETCH_ENABLED", false),
|
ScanFetchEnabled: envBool("SCAN_FETCH_ENABLED", false),
|
||||||
Dev: strings.EqualFold(env("APP_ENV", "dev"), "dev"),
|
Dev: strings.EqualFold(env("APP_ENV", "dev"), "dev"),
|
||||||
LogFile: env("LOG_FILE", ""),
|
LogFile: env("LOG_FILE", ""),
|
||||||
|
GitUserName: env("GIT_USER_NAME", ""),
|
||||||
|
GitUserEmail: env("GIT_USER_EMAIL", ""),
|
||||||
GiteaURL: env("GITEA_URL", ""),
|
GiteaURL: env("GITEA_URL", ""),
|
||||||
GiteaToken: env("GITEA_TOKEN", ""),
|
GiteaToken: env("GITEA_TOKEN", ""),
|
||||||
GitHubToken: env("GITHUB_TOKEN", ""),
|
GitHubToken: env("GITHUB_TOKEN", ""),
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ func (c *CLI) Version(ctx context.Context) (string, error) {
|
|||||||
return c.run(ctx, "", "version")
|
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.
|
// CurrentBranch returns the checked-out branch, or "HEAD" when detached.
|
||||||
func (c *CLI) CurrentBranch(ctx context.Context, dir string) (string, error) {
|
func (c *CLI) CurrentBranch(ctx context.Context, dir string) (string, error) {
|
||||||
return c.run(ctx, dir, "rev-parse", "--abbrev-ref", "HEAD")
|
return c.run(ctx, dir, "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
|||||||
Reference in New Issue
Block a user