import "github.com/sebastienrousseau/corral/internal/github"
Package github provides functionality to interact with the GitHub API.
func Token(ctx context.Context, authMode AuthMode) string
Token resolves a GitHub token for the given auth mode, returning an empty string when none can be obtained. It lets the git package authenticate HTTPS clones and pulls of private repositories with the same credential as the API.
func envToken() string
func isRetryableNetworkError(err error) bool
func matchesFilters(repo Repo, includeLang, excludeLang map[string]struct{}, opts FetchOptions) bool
func orgTypeForVisibility(v string) string
func rateLimitResetDuration(resp *http.Response) (time.Duration, bool)
func resolveToken(ctx context.Context, authMode AuthMode) (string, error)
func retryAfterDuration(resp *http.Response) (time.Duration, bool)
func shouldRetry(resp *http.Response, err error, attempt, maxRetries int) (bool, time.Duration)
func toLookupSet(values []string) map[string]struct{}
type AuthMode string
AuthMode controls how GitHub API credentials are resolved.
type FetchOptions struct {
// Limit caps the number of repositories returned; 0 means no limit.
Limit int
// Visibility filters repositories by visibility ("all", "public", or "private").
Visibility string
// IncludeForks includes forked repositories when true.
IncludeForks bool
// IncludeArchived includes archived repositories when true.
IncludeArchived bool
// IncludeLanguages, when non-empty, keeps only repositories matching these languages.
IncludeLanguages []string
// ExcludeLanguages removes repositories matching these languages.
ExcludeLanguages []string
// AuthMode selects how the GitHub token is resolved.
AuthMode AuthMode
// Type filters repositories by specific category (e.g. "sources", "forks", "archived", "mirrors", etc.).
Type string
// Sort specifies how the returned repositories list should be ordered.
Sort string
// RetryMax is the maximum number of retry attempts for transient failures.
RetryMax int
// RetryMinBackoff is the minimum delay between retry attempts.
RetryMinBackoff time.Duration
// RetryMaxBackoff is the maximum delay between retry attempts.
RetryMaxBackoff time.Duration
}
FetchOptions configures repository fetch behavior.
type Repo struct {
// Name is the repository name (without the owner prefix).
Name string
// Language is the primary programming language, or "Other" when unknown.
Language string
// Visibility is the normalized visibility, either "Public" or "Private".
Visibility string
// DefaultBranch is the repository's default branch name.
DefaultBranch string
// CloneURL is the HTTPS clone URL for the repository.
CloneURL string
// SSHURL is the SSH clone URL for the repository.
SSHURL string
// Fork reports whether the repository is a fork.
Fork bool
// Archived reports whether the repository is archived.
Archived bool
// PushedAt is the timestamp of the last push to any branch. The engine
// compares this against the cached value in <repo>/.corral-state.json to
// skip a `git pull` when nothing has changed upstream.
PushedAt time.Time
// Stars reports the stargazers count for the repository.
Stars int
// IsTemplate reports whether the repository is a template.
IsTemplate bool
// IsMirror reports whether the repository is a mirror.
IsMirror bool
// CanBeSponsored reports whether the repository has sponsorships enabled.
CanBeSponsored bool
}
Repo represents a simplified repository structure returned by the GitHub API.
type retryTransport struct {
base http.RoundTripper
maxRetries int
minBackoff time.Duration
maxBackoff time.Duration
}
func (t *retryTransport) RoundTrip(req *http.Request) (*http.Response, error)
RoundTrip implements http.RoundTripper, retrying transient failures and rate-limit responses with backoff until the request succeeds, becomes non-retryable, the retry budget is exhausted, or the context is cancelled.
func (t *retryTransport) backoff(attempt int) time.Duration
import "github.com/sebastienrousseau/corral/internal/git"
Package git provides helper functions to execute common Git commands by wrapping the system's git binary using os/exec.
func Clone(ctx context.Context, url, targetDir string, opts CloneOptions) error
Clone executes a git clone command for the given URL into the target directory.
func CurrentBranch(targetDir string) (string, error)
CurrentBranch retrieves the name of the currently checked-out branch.
func Pull(ctx context.Context, targetDir string, opts PullOptions) error
Pull executes a `git pull --rebase --autostash` in the target directory. Signature verification (merge.verifySignatures / rebase.verifySignatures) and commit signing (commit.gpgsign) are explicitly disabled for this invocation so an unattended sync never aborts on unsigned commits or blocks on a GPG/SSH passphrase prompt for users who sign commits globally. When opts.RecurseSubmodules is true: - if opts.IgnoreSubmoduleFailures is false, --recurse-submodules is appended to the pull so failures abort the whole operation (existing pre-v0.0.7 behaviour); - if opts.IgnoreSubmoduleFailures is true, the pull runs without --recurse-submodules and submodule updates are attempted in a separate `git submodule update --init --recursive` step whose error is logged but not returned.
func RemoteOrigin(targetDir string) (string, error)
RemoteOrigin retrieves the remote origin URL of the target directory by invoking `git remote get-url origin`. Prefer RemoteOriginFromConfig on hot paths (e.g. orphan detection over hundreds of clones) to avoid the per-call cost of spawning a subprocess.
func RemoteOriginFromConfig(targetDir string) (string, error)
RemoteOriginFromConfig parses the `url =` entry under [remote "origin"] directly from <targetDir>/.git/config, avoiding the ~5-15ms per-call cost of spawning `git remote get-url origin`. Returns the wrapped os.ErrNotExist when the config file is absent, and a clear error when the section or key is missing. Tolerates blank lines, `#` / `;` comments, indented entries, and CRLF line endings.
func ResolveGitBinary() error
ResolveGitBinary looks up the absolute path to the git executable on PATH and caches it for the rest of the process. Returns a clear error when git is not installed.
func authEnv() []string
authEnv returns the environment variables that inject an Authorization header for github.com HTTPS requests, or nil when no token is available. The header is scoped to https://github.com/, so it is harmless for SSH remotes.
func nonInteractiveEnv() []string
nonInteractiveEnv returns the environment variables that force git into strict non-interactive mode. They are applied unconditionally to every git invocation so unattended runs (cron, CI) never hang on a missing credential, askpass helper, or GPG pinentry.
func updateSubmodules(ctx context.Context, targetDir string) error
updateSubmodules runs `git submodule update --init --recursive` in targetDir as a separate subprocess. Exposed indirectly via Pull's IgnoreSubmoduleFailures branch.
func withGitEnv(cmd *exec.Cmd)
withGitEnv attaches the credentials header (when available) plus the non-interactive env vars to cmd, replacing any prior cmd.Env. It always sets cmd.Env so the non-interactive guards apply even on anonymous clones.
type CloneOptions struct {
// RecurseSubmodules, when true, clones submodules recursively by adding
// the --recurse-submodules flag.
RecurseSubmodules bool
// SingleBranch, when true, clones only the history of the default branch
// by adding the --single-branch flag.
SingleBranch bool
// Blobless, when true, performs a blobless partial clone by adding the
// --filter=blob:none flag, deferring blob downloads until needed.
Blobless bool
// Depth, when greater than zero, creates a shallow clone truncated to the
// given number of commits by adding the --depth flag.
Depth int
}
CloneOptions configures optional clone-time performance and layout flags.
type PullOptions struct {
// RecurseSubmodules, when true, also updates submodules after the pull.
// When IgnoreSubmoduleFailures is set, the submodule update runs as a
// separate step so its failure does not abort the parent pull.
RecurseSubmodules bool
// IgnoreSubmoduleFailures, when true, logs (but does not propagate)
// errors from the post-pull submodule update step. Useful when a
// submodule has been deleted upstream or access has been revoked but
// the parent repository's history should still update.
IgnoreSubmoduleFailures bool
}
PullOptions configures a `git pull` invocation.
import "github.com/sebastienrousseau/corral/internal/engine"
Package engine provides the core concurrency and execution logic for Corral.
func Run(ctx context.Context, opts RunOptions)
Run executes the core Corral workflow, orchestrating GitHub API fetches, legacy layout migrations, concurrent Git operations, and orphaned repository detection.
func cleanupEmptyFolders(baseDir string, repos []github.Repo)
cleanupEmptyFolders removes the now-empty legacy top-level language directories left behind by migrateLegacy. It only targets directories whose names match a repository language, and os.Remove deletes a directory only when it is empty, so unrelated entries under baseDir (e.g. .claude, other projects) are never touched.
func detectOrphans(owner, baseDir string, repos []github.Repo)
func evaluateLayout(layoutTpl string, repo github.Repo, owner string) (string, error)
func migrateLegacy(baseDir string, repos []github.Repo)
func normalizeLanguage(lang string) string
func normalizeLanguageDirCase(baseDir string, repos []github.Repo)
normalizeLanguageDirCase renames any case-variant language subdirectory under each visibility directory to its lowercase form. On case-insensitive filesystems (APFS, HFS+, NTFS) a direct os.Rename("JavaScript","javascript") is a silent no-op, so the rename is performed via a temporary name. Only directories whose lowercased name matches a normalized language from the fetched repos are touched, so unrelated entries (e.g. "Configurations") are left alone.
func repoNameFromURL(url string) string
repoNameFromURL extracts the repository name from a git remote URL, stripping any trailing ".git" suffix. It returns an empty string when no segment exists.
func stampCloneState(targetDir string, repo github.Repo)
stampCloneState records the upstream pushed_at in the per-clone state sidecar so the next run can skip a no-op git pull. Best-effort: a write failure is logged but does not fail the operation, since the sidecar is purely an optimization (a missing or stale file falls through to the pre-sidecar behaviour of always pulling).
func toLogMsg(msg RepoResult) tui.LogMsg
func writeCloneState(repoDir string, s cloneState) error
writeCloneState serialises s to repoDir/.corral-state.json atomically by writing to a sibling temp file and renaming it into place. A crash mid-write therefore leaves the previous valid state on disk rather than a half-written file that would fail to parse on the next run.
type Job struct {
// Repo is the GitHub repository to be processed.
Repo github.Repo
// Target is the destination directory for the repository under the new layout.
Target string
// Legacy is the directory where the repository may exist under the old layout.
Legacy string
}
Job encapsulates a repository to be processed along with its target directories.
type OutputFormat string
OutputFormat controls how operation results are emitted.
type RepoResult struct {
// RepoName is the name of the processed repository.
RepoName string `json:"repo"`
// Action is the outcome verb, such as CLONE, SYNC, SKIP, ERROR, or DRY-RUN.
Action string `json:"action"`
// Message is a human-readable description of the outcome.
Message string `json:"message"`
// Target is the destination directory for the repository.
Target string `json:"target"`
// Visibility is the repository visibility (e.g. Public or Private).
Visibility string `json:"visibility"`
// Language is the normalized primary language directory name.
Language string `json:"language"`
// DryRun indicates whether the run was performed in dry-run mode.
DryRun bool `json:"dry_run"`
// Protocol is the clone transport used (https or ssh).
Protocol string `json:"protocol"`
// ClonedURL is the URL used for cloning, if a clone was attempted.
ClonedURL string `json:"clone_url,omitempty"`
// SyncAttempt indicates whether a sync (pull) was attempted.
SyncAttempt bool `json:"sync_attempt"`
}
RepoResult represents the final status of processing a repository.
type RunOptions struct {
// Owner is the GitHub user or organization whose repositories are processed.
Owner string
// BaseDir is the root directory under which repositories are laid out.
BaseDir string
// Concurrency is the number of worker goroutines processing repositories; must be >= 1.
Concurrency int
// DryRun, when true, reports intended actions without performing clone or pull operations.
DryRun bool
// Orphans, when true, enables detection of local repositories no longer present upstream.
Orphans bool
// Protocol selects the clone transport and must be either "https" or "ssh".
Protocol string
// DoSync, when true, pulls updates into existing repositories.
DoSync bool
// Output selects the result emission format (text, json, or ndjson).
Output OutputFormat
// Interactive, when true, displays an interactive selector before processing.
Interactive bool
// Fetch holds the options passed to the GitHub repository listing call.
Fetch github.FetchOptions
// Clone holds the options passed to each Git clone operation.
Clone git.CloneOptions
// Sync controls when an already-cloned repository is actually pulled.
Sync SyncOptions
// Layout specifies the templated path structure for repositories.
Layout string
// Version is the build version of Corral.
Version string
}
RunOptions contains all execution controls for a run.
type Summary struct {
// Total is the number of repositories scheduled for processing.
Total int `json:"total"`
// Cloned is the number of repositories successfully cloned.
Cloned int `json:"cloned"`
// Synced is the number of repositories successfully synced.
Synced int `json:"synced"`
// Skipped is the number of repositories skipped.
Skipped int `json:"skipped"`
// Failed is the number of repositories that failed to process.
Failed int `json:"failed"`
}
Summary tracks aggregate run outcomes.
func (s *Summary) add(msg RepoResult)
type SyncOptions struct {
// Force, when true, runs `git pull` even when the cached state shows
// the upstream pushed_at is unchanged.
Force bool
// IgnoreSubmoduleFailures, when true with Clone.RecurseSubmodules, allows
// the parent repository to update even when a submodule sync fails (e.g.
// the submodule repo was deleted upstream or its access revoked). The
// failure is logged as a WARN but not propagated.
IgnoreSubmoduleFailures bool
}
SyncOptions configures the engine's per-repo sync decision. Kept separate from git.CloneOptions because forcing a sync is a corral-level policy choice, not a clone-time git flag.
type cloneState struct {
// LastSyncedPushedAt is the upstream PushedAt value at the time of the
// last successful clone or sync.
LastSyncedPushedAt time.Time `json:"last_synced_pushed_at"`
// LastSyncedAt is the local wall-clock time of the last sync attempt
// that touched the working tree (clone or successful pull). Used for
// human display only — never for sync-skip decisions.
LastSyncedAt time.Time `json:"last_synced_at"`
}
cloneState is the JSON shape of <repo>/.corral-state.json. New fields must be added with omitempty so older sidecars continue to round-trip.
import "github.com/sebastienrousseau/corral/internal/tui"
Package tui provides a Bubble Tea terminal user interface for Corral.
func GetStyledLogo() string
GetStyledLogo returns the colored ASCII logo art as a string.
func NewModel(total int) tea.Model
NewModel initializes a new TUI model with the expected total number of items.
func RunSelector(ctx context.Context, owner string, fetchOpts github.FetchOptions, fetchFn FetchFunc) ([]github.Repo, bool, error)
RunSelector launches the interactive terminal selector program to choose repositories.
type FetchFunc func() ([]github.Repo, error)
FetchFunc represents the function signature used by the selector to fetch repositories.
type LogMsg struct {
// RepoName is the name of the repository the entry refers to.
RepoName string
// Action is the operation performed (for example CLONE, SYNC, SKIP, ERROR).
Action string
// Message is a human-readable description of the outcome.
Message string
}
LogMsg represents a log entry to be displayed in the TUI.
type fetchedReposMsg struct {
repos []github.Repo
err error
}
type model struct {
total int
done int
logs []LogMsg
prog progress.Model
quitting bool
cloned int
synced int
failed int
existing int
}
model represents the state of the Bubble Tea application.
func (m model) Init() tea.Cmd
Init initializes the Bubble Tea application (no-op).
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd)
Update handles incoming Bubble Tea messages, advancing progress and stats as repository results arrive and quitting when the run completes or is cancelled.
func (m model) View() string
View renders the current progress bar, recent log lines, and, once finished, the final summary of the run.
func (m *model) processLogMsg(msg LogMsg)
type selectorModel struct {
repos []github.Repo
filteredRepos []github.Repo
filter string
selected map[string]bool // key is repo.Name
table table.Model
spinner spinner.Model
loading bool
loadingErr error
confirmed bool
quitting bool
fetchFn FetchFunc
showHelp bool
cmdErr string
}
func (m *selectorModel) Init() tea.Cmd
func (m *selectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd)
func (m *selectorModel) View() string
func (m *selectorModel) applyFilter()
func (m *selectorModel) executeSlashCommand(cmdStr string) tea.Cmd
func (m *selectorModel) renderCustomTable() string
func (m *selectorModel) renderFooter() string
func (m *selectorModel) renderHelpPanel() string
func (m *selectorModel) updateTableRows()