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).
Port of macos/RmoteMacApp/Server/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 ¶
- Variables
- func IsValidBranchName(name string) bool
- func IsValidRef(ref string) bool
- func ProcessCwd(pid int) string
- type CommitMessageResult
- type CommitRequest
- type DiffRequest
- type GitBranch
- type GitCommit
- type GitCommitResponse
- type GitDiff
- type GitEntry
- type GitLogResponse
- type GitPushResponse
- type GitStatus
- type LogRequest
- type PathValidator
- type Result
- type Service
- func (s *Service) BranchesJSON(cwd string) ([]byte, error)
- func (s *Service) CheckoutJSON(cwd, branch string) ([]byte, error)
- func (s *Service) CommitJSON(req CommitRequest) ([]byte, error)
- func (s *Service) CommitMessage(cwd string, paths []string, agent string) CommitMessageResult
- func (s *Service) DiffJSON(req DiffRequest) ([]byte, error)
- func (s *Service) DiscardJSON(cwd string, paths []string) ([]byte, error)
- func (s *Service) LogJSON(req LogRequest) ([]byte, error)
- func (s *Service) MergeJSON(cwd, branch string) ([]byte, error)
- func (s *Service) PullJSON(cwd string) ([]byte, error)
- func (s *Service) PushJSON(cwd string) ([]byte, error)
- func (s *Service) ResetJSON(cwd, commit string) ([]byte, error)
- func (s *Service) RevertJSON(cwd, commit string) ([]byte, error)
- func (s *Service) Run(cwd string, args ...string) (*Result, error)
- func (s *Service) RunSuccess(cwd string, args ...string) ([]byte, error)
- func (s *Service) RunTimeout(cwd string, timeout time.Duration, args ...string) (*Result, error)
- func (s *Service) ShowJSON(cwd, commit string) ([]byte, error)
- func (s *Service) StageJSON(cwd string, paths []string, stage bool) ([]byte, error)
- func (s *Service) StatusJSON(cwd string) ([]byte, error)
Constants ¶
This section is empty.
Variables ¶
var ErrDetachedHead = errors.New("git: detached HEAD (no branch to push)")
ErrDetachedHead is returned when push is attempted on a detached HEAD.
var ErrGitNotFound = errors.New("git: binary not found on PATH")
ErrGitNotFound is returned when no `git` binary is on PATH.
var ErrIdentityMissing = errors.New("git: identity missing (configure user.email)")
ErrIdentityMissing is returned when git user.email isn't configured.
var ErrNoUpstream = errors.New("git: no upstream configured for this branch")
ErrNoUpstream is returned when the branch has no upstream tracking.
var ErrPathNotAllowed = errors.New("git: path not allowed")
ErrPathNotAllowed is returned when cwd is outside the allowlist.
var ErrTimeout = errors.New("git: command timed out")
ErrTimeout is returned when the git command exceeds the timeout.
Functions ¶
func IsValidBranchName ¶
IsValidBranchName rejects shell metacharacters. Allows alphanumerics, dash, underscore, slash, dot — the standard git branch name charset.
func IsValidRef ¶
IsValidRef validates a commit ref (hash, short hash, or refname). Same charset as branch names plus the refspec characters git accepts.
func ProcessCwd ¶
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.
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 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 ¶
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 ¶
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 ¶
BranchesJSON lists local branches. `git branch --format=...` with a custom format that marks the current branch with `*`.
func (*Service) CheckoutJSON ¶
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:
- Check identity (user.email configured) → ErrIdentityMissing
- Sanitize message (strip NUL/VT/FF, trim, cap)
- git add -- <paths>
- git commit -m <msg>
- 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 ¶
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) PushJSON ¶
PushJSON pushes the current branch to its upstream. Multi-step:
- Check branch is not detached → ErrDetachedHead
- Check upstream exists → ErrNoUpstream
- git push --no-progress -- <remote> <branch>
- NEVER uses --force (matches Mac safety).
func (*Service) ResetJSON ¶
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 ¶
RevertJSON reverts a commit. Uses --no-edit (no commit message editor).
func (*Service) Run ¶
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 ¶
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 ¶
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 ¶
ShowJSON returns the patch for a commit. `git show --no-color <commit>`. Capped at maxDiffBytes (4 MB).
func (*Service) StageJSON ¶
StageJSON stages or unstages paths. stage=true → git add; stage=false → git reset (unstage).
func (*Service) StatusJSON ¶
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.