git

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package git implements the daemon's git operation surface — a thin shell- out wrapper around the system `git` binary with path allowlisting, timeout, and byte caps. The daemon does NOT embed a git library (libgit2 etc.); it execs the host's git so behavior matches exactly what the user sees on the command line (same config, hooks, credentials).

Implementation of Git/GitService.swift (~2400 LOC). The Go port is ~30% smaller because Go's exec.Command + bytes.Buffer replaces Swift's Process + Pipe ceremony, and the path allowlist is shared with the security package (the Mac has its own PathValidator).

Security: every Run call validates cwd against security.PathAllowlist ($HOME + ~/.rmote/allowed-paths.txt). A git operation outside the allowlist returns ErrPathNotAllowed (HTTP 403). This prevents a compromised token from reading repo state in /etc or /root.

Index

Constants

This section is empty.

Variables

View Source
var ErrDetachedHead = errors.New("git: detached HEAD (no branch to push)")

ErrDetachedHead is returned when push is attempted on a detached HEAD.

View Source
var ErrGitNotFound = errors.New("git: binary not found on PATH")

ErrGitNotFound is returned when no `git` binary is on PATH.

View Source
var ErrIdentityMissing = errors.New("git: identity missing (configure user.email)")

ErrIdentityMissing is returned when git user.email isn't configured.

View Source
var ErrNoUpstream = errors.New("git: no upstream configured for this branch")

ErrNoUpstream is returned when the branch has no upstream tracking.

View Source
var ErrPathNotAllowed = errors.New("git: path not allowed")

ErrPathNotAllowed is returned when cwd is outside the allowlist.

View Source
var ErrTimeout = errors.New("git: command timed out")

ErrTimeout is returned when the git command exceeds the timeout.

Functions

func IsValidBranchName

func IsValidBranchName(name string) bool

IsValidBranchName rejects shell metacharacters. Allows alphanumerics, dash, underscore, slash, dot — the standard git branch name charset.

func IsValidRef

func IsValidRef(ref string) bool

IsValidRef validates a commit ref (hash, short hash, or refname). Same charset as branch names plus the refspec characters git accepts.

func ProcessCwd

func ProcessCwd(pid int) string

ProcessCwd returns the current working directory of the process with the given PID. On Linux, reads /proc/<pid>/cwd (symlink). On macOS, shells out to `lsof`. Returns "" if the cwd can't be determined (process exited, permission denied, etc.) — the caller falls back to the session's stored cwd.

func ValidateRelativePath added in v0.2.0

func ValidateRelativePath(p, cwd string) (string, bool)

ValidateRelativePath ensures p is relative + stays under cwd. Returns the cleaned relative path, or ok=false on absolute, traversal, or escape.

Types

type CommitMessageResult

type CommitMessageResult struct {
	Message   string `json:"message"`
	Available bool   `json:"available"`
}

CommitMessageResult is the JSON response shape. iOS's GitCommitMessageResponse REQUIRES both fields or the decode fails.

type CommitRequest

type CommitRequest struct {
	Cwd     string
	Paths   []string // relative paths to stage + commit
	Message string   // commit message
}

CommitRequest carries the commit body.

type DiffRequest

type DiffRequest struct {
	Cwd    string
	Path   string // required, relative to cwd
	Ref    string // optional: show diff for this ref
	Staged bool   // optional: diff --cached
	Full   bool   // optional: raise line cap to 50000
}

DiffRequest carries the query params for a diff lookup.

type GitBranch

type GitBranch struct {
	Name    string `json:"name"`
	Current bool   `json:"current"`
}

GitBranch is one entry in the branch list. Matches iOS Codable.

type GitCommit

type GitCommit struct {
	Hash       string   `json:"hash"`
	ShortHash  string   `json:"short_hash"`
	Subject    string   `json:"subject"`
	Author     string   `json:"author"`
	AuthorDate string   `json:"author_date"` // ISO-8601 (%aI)
	IsMerge    bool     `json:"is_merge"`
	Body       string   `json:"body"`     // always "" here (fetched on-demand by commit-detail)
	Parents    []string `json:"parents"`  // full hashes from %P (NOT %p) — graph correctness
	Branches   []string `json:"branches"` // branch tips pointing at this commit
	Tags       []string `json:"tags"`     // tag tips pointing at this commit
}

GitCommit is one entry in the log. Matches iOS GitCommit Codable.

type GitCommitResponse

type GitCommitResponse struct {
	OK    bool   `json:"ok"`
	Short string `json:"short,omitempty"` // new HEAD short rev
}

GitCommitResponse is the response for POST /api/git/commit.

type GitDiff

type GitDiff struct {
	Path      string `json:"path"`
	Staged    bool   `json:"staged"`
	Ref       string `json:"ref,omitempty"` // only present when a ref was queried
	Text      string `json:"text"`          // empty when binary
	Binary    bool   `json:"binary"`
	Truncated bool   `json:"truncated"`
}

GitDiff is the response for the diff route. Matches iOS GitDiff Codable.

type GitEntry

type GitEntry struct {
	Path    string `json:"path"`
	OldPath string `json:"old_path,omitempty"` // rename source (format 2 only)
	Status  string `json:"status"`             // porcelain-v2 XY, padded to 2 chars; "??" for untracked
	Adds    *int   `json:"adds,omitempty"`     // lines added (numstat); nil if binary/untracked
	Dels    *int   `json:"dels,omitempty"`     // lines deleted (numstat); nil if binary/untracked
}

GitEntry is one file in the status response. Matches iOS GitEntry Codable.

type GitLogResponse

type GitLogResponse struct {
	Commits []GitCommit `json:"commits"`
	Limit   int         `json:"limit"`
	Head    string      `json:"head,omitempty"` // full hash of current HEAD; empty on unborn repo
}

GitLogResponse is the response for GET /api/git/log.

type GitPushResponse

type GitPushResponse struct {
	OK bool `json:"ok"`
}

GitPushResponse is the response for POST /api/git/push.

type GitStatus

type GitStatus struct {
	Branch    string     `json:"branch"` // branch name or short rev if detached
	Upstream  string     `json:"upstream,omitempty"`
	Ahead     int        `json:"ahead"`
	Behind    int        `json:"behind"`
	Changes   []GitEntry `json:"changes"`   // tracked modified/added/renamed/deleted
	Untracked []GitEntry `json:"untracked"` // untracked files
	Truncated bool       `json:"truncated"` // true if entry cap hit
	InMerge   bool       `json:"in_merge"`  // mid-merge/mid-revert
}

GitStatus is the response shape for GET /api/git/status. Matches iOS GitStatus Codable field names verbatim.

func ParseStatus

func ParseStatus(output []byte) GitStatus

ParseStatus parses `git status --porcelain=v2 --untracked-files=all -z -b` output into a GitStatus struct. The output is NUL-separated records; each record is a single line. Branch headers start with `# `.

type LogRequest

type LogRequest struct {
	Cwd   string
	Limit int  // clamped 1..200, default 200
	Skip  int  // >= 0, default 0
	All   bool // include local + remote branches (excluding tool-owned Entire refs)
}

LogRequest carries query params for the log lookup.

type PathValidator

type PathValidator interface {
	Validate(path string) bool
}

PathValidator is the interface for path-allowlist checks. Implemented by security.PathAllowlist; extracted as an interface so tests can substitute a test double without depending on $HOME.

type Result

type Result struct {
	Stdout    []byte
	Stderr    []byte
	ExitCode  int  // 0 = success; non-zero = git reported an error (stderr has the message)
	Truncated bool // stdout or stderr exceeded the shared capture cap
}

Result captures one git invocation's output.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service wraps the system git binary with path validation + timeout. One instance serves all /api/git/* requests; goroutine-safe via stateless Run (each call spawns its own exec.Command).

func NewService

func NewService() *Service

NewService builds a git service with the default path allowlist. The allowlist confines operations to $HOME + user-added paths.

func (*Service) BranchesJSON

func (s *Service) BranchesJSON(cwd string) ([]byte, error)

BranchesJSON lists local branches. `git branch --format=...` with a custom format that marks the current branch with `*`.

func (*Service) CheckoutJSON

func (s *Service) CheckoutJSON(cwd, branch string) ([]byte, error)

CheckoutJSON switches to a branch. Validates the branch name (no shell metacharacters) before passing to git.

func (*Service) CommitJSON

func (s *Service) CommitJSON(req CommitRequest) ([]byte, error)

CommitJSON stages paths + commits. Multi-step:

  1. Check identity (user.email configured) → ErrIdentityMissing
  2. Sanitize message (strip NUL/VT/FF, trim, cap)
  3. git add -- <paths>
  4. git commit -m <msg>
  5. git rev-parse --short HEAD

func (*Service) CommitMessage

func (s *Service) CommitMessage(cwd string, paths []string, agent string) CommitMessageResult

CommitMessage runs the agent CLI to draft a single Conventional Commits subject for the selection. Returns available:false on ANY failure (the route is always 200; never an error token).

func (*Service) DiffJSON

func (s *Service) DiffJSON(req DiffRequest) ([]byte, error)

DiffJSON runs the appropriate git diff command and returns JSON-encoded GitDiff. Validates cwd via Service.Run. The 4-way branching matches the Mac exactly:

ref set      → git show --no-color --format= --diff-merges=first-parent <ref> -- <path>
staged=true  → git diff --no-color --cached -- <path>
untracked    → git diff --no-color --no-index -- /dev/null <path>
else         → git diff --no-color -- <path>

Untracked detection: a separate `git ls-files --others --exclude-standard` check confirms the path is actually untracked before running --no-index (which would error on a tracked path).

func (*Service) DiscardJSON

func (s *Service) DiscardJSON(cwd string, paths []string) ([]byte, error)

DiscardJSON discards working-tree changes for paths. Uses git checkout -- <paths> (NOT git reset --hard, which would also reset staged content).

func (*Service) LogJSON

func (s *Service) LogJSON(req LogRequest) ([]byte, error)

LogJSON runs `git log` + for-each-ref and returns the JSON-encoded GitLogResponse. Uses %P (full parent hashes) not %p (abbreviated) for graph layout correctness. --topo-order is mandatory (child-before-parent). The all-branches view preserves every commit and its real parents. The iOS graph layout needs those intermediate commits to keep a branch lane continuous; decoration simplification rewrites ancestry and collapses branches into short tip-to-ancestor arcs.

func (*Service) MergeJSON

func (s *Service) MergeJSON(cwd, branch string) ([]byte, error)

MergeJSON merges a branch into the current one.

func (*Service) PullJSON

func (s *Service) PullJSON(cwd string) ([]byte, error)

PullJSON pulls from the upstream. Refuses detached HEAD.

func (*Service) PushJSON

func (s *Service) PushJSON(cwd string) ([]byte, error)

PushJSON pushes the current branch to its upstream. Multi-step:

  1. Check branch is not detached → ErrDetachedHead
  2. Check upstream exists → ErrNoUpstream
  3. git push --no-progress -- <remote> <branch>
  4. NEVER uses --force (matches Mac safety).

func (*Service) ResetJSON

func (s *Service) ResetJSON(cwd, commit string) ([]byte, error)

ResetJSON hard-resets to a commit. DANGEROUS — discards uncommitted changes. Audit-logged by the caller. The caller MUST confirm with the user.

func (*Service) RevertJSON

func (s *Service) RevertJSON(cwd, commit string) ([]byte, error)

RevertJSON reverts a commit. Uses --no-edit (no commit message editor).

func (*Service) Run

func (s *Service) Run(cwd string, args ...string) (*Result, error)

Run executes `git <args>` in cwd. Validates cwd first, resolves the git binary lazily (cached), then execs with a timeout + output cap. A non-zero exit code is NOT an error from Run's perspective — it's returned in Result.ExitCode so the caller can surface the git error message to the user (e.g., "nothing to commit" is exit 1 but not a daemon error).

Returns an error only for: path not allowed, git not found, timeout, or spawn failure. These are HTTP 4xx/5xx level failures.

func (*Service) RunSuccess

func (s *Service) RunSuccess(cwd string, args ...string) ([]byte, error)

RunSuccess is a convenience that runs git + returns stdout on success, or the stderr message on non-zero exit. Used by read-only routes where the caller just wants the output or the error string.

func (*Service) RunTimeout

func (s *Service) RunTimeout(cwd string, timeout time.Duration, args ...string) (*Result, error)

RunTimeout is Run with a caller-supplied deadline capped by the service default. Multi-command read surfaces use it to enforce one aggregate budget.

func (*Service) ShowJSON

func (s *Service) ShowJSON(cwd, commit string) ([]byte, error)

ShowJSON returns the patch for a commit. `git show --no-color <commit>`. Capped at maxDiffBytes (4 MB).

func (*Service) StageJSON

func (s *Service) StageJSON(cwd string, paths []string, stage bool) ([]byte, error)

StageJSON stages or unstages paths. stage=true → git add; stage=false → git reset (unstage).

func (*Service) StatusJSON

func (s *Service) StatusJSON(cwd string) ([]byte, error)

StatusJSON runs `git status --porcelain=v2` + numstat + detached-HEAD resolution and returns the JSON-encoded GitStatus. cwd is validated by the Service.Run call. Best-effort numstat — if it fails (e.g., empty repo), adds/dels are simply nil.

func (*Service) ValidateCwd added in v0.2.0

func (s *Service) ValidateCwd(cwd string) bool

ValidateCwd applies the same canonical path policy used before every Git command. Callers that must touch repository files directly use this first so they cannot bypass the service's allowlist.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL