Add "Git-Operations"

Virgil 2026-02-19 17:02:15 +00:00
parent 1f69ce942c
commit 1ec7cac357

196
Git-Operations.-.md Normal file

@ -0,0 +1,196 @@
# Git Operations
**Package:** `forge.lthn.ai/core/go-scm/git`
Multi-repository git utilities providing parallel status checks, push/pull operations, and integration with the Core framework as a registered service.
## Overview
The `git` package wraps `git` CLI commands to operate across multiple repositories simultaneously. It is used by the `core dev` CLI commands (`work`, `status`, `push`, `pull`) to manage the federated monorepo.
## RepoStatus
The `RepoStatus` struct captures a snapshot of a single repository's state:
```go
type RepoStatus struct {
Name string // Display name
Path string // Filesystem path
Modified int // Files modified in working tree
Untracked int // Untracked files
Staged int // Files staged in index
Ahead int // Commits ahead of upstream
Behind int // Commits behind upstream
Branch string // Current branch name
Error error // Any error during status check
}
```
### Helper Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `IsDirty()` | `bool` | Has uncommitted changes (Modified > 0 or Untracked > 0 or Staged > 0) |
| `HasUnpushed()` | `bool` | Has commits to push (Ahead > 0) |
| `HasUnpulled()` | `bool` | Has commits to pull (Behind > 0) |
## Parallel Status Checks
The `Status` function checks multiple repositories concurrently using goroutines and a WaitGroup:
```go
statuses := git.Status(ctx, git.StatusOptions{
Paths: []string{
"/Users/snider/Code/host-uk/core-php",
"/Users/snider/Code/host-uk/core-tenant",
"/Users/snider/Code/host-uk/core-admin",
},
Names: map[string]string{
"/Users/snider/Code/host-uk/core-php": "core-php",
"/Users/snider/Code/host-uk/core-tenant": "core-tenant",
"/Users/snider/Code/host-uk/core-admin": "core-admin",
},
})
for _, s := range statuses {
if s.Error != nil {
fmt.Printf("%s: error: %v\n", s.Name, s.Error)
continue
}
if s.IsDirty() {
fmt.Printf("%s [%s]: %d modified, %d untracked, %d staged\n",
s.Name, s.Branch, s.Modified, s.Untracked, s.Staged)
}
if s.HasUnpushed() {
fmt.Printf("%s: %d commits ahead\n", s.Name, s.Ahead)
}
}
```
### StatusOptions
```go
type StatusOptions struct {
Paths []string // Repository paths to check
Names map[string]string // Maps paths to display names
}
```
## Push and Pull
Push and pull operations use interactive mode to support SSH passphrase prompts:
```go
// Push a single repository
err := git.Push(ctx, "/path/to/repo")
// Pull a single repository (with --rebase)
err := git.Pull(ctx, "/path/to/repo")
// Check if error is a non-fast-forward rejection
if git.IsNonFastForward(err) {
fmt.Println("Need to pull first")
}
```
### PushMultiple
Push multiple repositories sequentially (sequential because SSH passphrase prompts need user interaction):
```go
results := git.PushMultiple(ctx, paths, names)
for _, r := range results {
if r.Success {
fmt.Printf("%s: pushed successfully\n", r.Name)
} else {
fmt.Printf("%s: push failed: %v\n", r.Name, r.Error)
}
}
```
#### PushResult
```go
type PushResult struct {
Name string
Path string
Success bool
Error error
}
```
## GitError
Git command errors include captured stderr output for better diagnostics:
```go
type GitError struct {
Err error // Underlying exec error
Stderr string // Captured stderr output
}
func (e *GitError) Error() string // Returns stderr if available, else Err.Error()
func (e *GitError) Unwrap() error // Returns Err for error chain inspection
```
## Core Service Integration
The `git` package integrates with the Core framework as a registered service, exposing git operations via the query/task message-passing system:
```go
import (
"forge.lthn.ai/core/go/pkg/framework"
"forge.lthn.ai/core/go-scm/git"
)
core, err := framework.New(
framework.WithService(git.NewService(git.ServiceOptions{
WorkDir: "/Users/snider/Code/host-uk",
})),
)
```
### Queries
| Query Type | Returns | Description |
|-----------|---------|-------------|
| `QueryStatus{Paths, Names}` | `[]RepoStatus` | Run parallel status check |
| `QueryDirtyRepos{}` | `[]RepoStatus` | Get repos with uncommitted changes (from last status) |
| `QueryAheadRepos{}` | `[]RepoStatus` | Get repos with unpushed commits (from last status) |
### Tasks
| Task Type | Description |
|----------|-------------|
| `TaskPush{Path, Name}` | Push a single repository |
| `TaskPull{Path, Name}` | Pull a single repository |
| `TaskPushMultiple{Paths, Names}` | Push multiple repositories sequentially |
### Service Methods
The service also exposes direct method access:
```go
svc := framework.ServiceFor[*git.Service](core)
// Get last status results
statuses := svc.Status()
// Get filtered views
dirty := svc.DirtyRepos()
ahead := svc.AheadRepos()
```
## Implementation Details
- **Parallel execution**: `Status` uses one goroutine per repository with `sync.WaitGroup`, maintaining result order via indexed slice
- **Branch detection**: Uses `git rev-parse --abbrev-ref HEAD`
- **Status parsing**: Uses `git status --porcelain` and parses the two-character status codes (X = index, Y = working tree)
- **Ahead/behind**: Uses `git rev-list --count @{u}..HEAD` and `HEAD..@{u}` respectively; silently returns 0 if no upstream is configured
- **Interactive push/pull**: Connects stdin/stdout/stderr to the terminal for SSH passphrase prompts, while also capturing stderr via `io.MultiWriter` for error reporting
## See Also
- [[Home]] -- Package overview and quick start
- [[Job-Runner]] -- Automated pipeline that may trigger push operations