diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index be2190e..f122317 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -178,6 +178,30 @@ Summary: total: 8 | cloned: 1 | updated: 1 | dirty: 1 | branch-drift: 1 | unknown: 1 | excluded-but-present: 1 | errors: 0 ``` +### Clone-Only Mode + +Using `--clone` to quickly clone only missing repositories without processing existing ones: + +``` +$ ghorgsync --clone + repo new-service [cloned] + repo new-library [cloned] + +Summary: + total: 10 | cloned: 2 | updated: 0 | dirty: 0 | branch-drift: 0 | unknown: 0 | excluded-but-present: 0 | errors: 0 +``` + +When all repositories are already cloned locally, `--clone` finishes quickly with no output: + +``` +$ ghorgsync --clone + +Summary: + total: 10 | cloned: 0 | updated: 0 | dirty: 0 | branch-drift: 0 | unknown: 0 | excluded-but-present: 0 | errors: 0 +``` + +This mode skips all per-repository processing (fetch, dirty check, checkout, pull) and directory auditing (collisions, unknown folders, excluded-but-present), making it significantly faster when you only need to pull down new repositories. + ## Troubleshooting ### Missing Dotfile diff --git a/docs/USAGE.md b/docs/USAGE.md index b51c810..9e141b9 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -60,6 +60,11 @@ ghorgsync [flags] | `--version` | Print version and exit | | `--verbose` | Enable verbose output (show per-repository processing detail) | | `--no-color` | Disable color output | +| `--clone` | Clone-only mode: only clone missing repositories (see [Clone-Only Mode](#clone-only-mode)) | + +### Mode Flags + +The `--clone` flag is a mode flag that changes the sync behavior. Mode flags are mutually exclusive; if multiple mode flags are provided, the command exits with an error. ## Runtime Behavior @@ -83,6 +88,24 @@ When invoked, **ghorgsync** performs the following steps: During the repository clone/process phase, **ghorgsync** shows a live progress bar on TTY output to indicate completion across managed repositories. +### Clone-Only Mode + +When invoked with `--clone`, **ghorgsync** runs a streamlined workflow focused exclusively on cloning missing repositories: + +1. **Load configuration** and **resolve authentication** (same as default mode). +2. **Fetch the organization repository list** and **filter repositories** (same as default mode). +3. **Scan the local directory** to identify which included repositories are missing. +4. **Clone missing repositories** only. +5. **Print a summary line** with counts. + +**What is skipped** compared to the default workflow: + +- Processing existing repositories (fetch, dirty check, checkout, pull) is entirely skipped. +- Collisions, unknown folders, and excluded-but-present findings are not reported. +- The progress bar only covers missing repositories to clone. + +This mode is useful when you know a new repository has been added to the organization and you want to quickly pull it down without waiting for every existing repository to be fetched and checked. + ### Per-Repository Processing For each included repository that exists locally: diff --git a/main.go b/main.go index 4636216..107be6b 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ func main() { versionFlag := flag.Bool("version", false, "Print version and exit") verboseFlag := flag.Bool("verbose", false, "Enable verbose output") noColorFlag := flag.Bool("no-color", false, "Disable color output") + cloneOnlyFlag := flag.Bool("clone", false, "Only clone missing repositories (skip processing existing repos)") flag.Parse() if *versionFlag { @@ -106,44 +107,60 @@ func main() { // Summary counters var summary model.Summary summary.TotalRepos = len(included) - summary.UnknownFolders = len(scanResult.Unknown) - summary.ExcludedButPresent = len(scanResult.ExcludedButPresent) - summary.Errors = len(scanResult.Collisions) - - repoWorkTotal := len(scanResult.ManagedMissing) + len(scanResult.ManagedFound) - printer.StartRepoProgress(repoWorkTotal) - - // Clone missing repos - for _, name := range scanResult.ManagedMissing { - repo := repoMap[name] - result := eng.CloneRepo(repo) - handleResult(printer, result, &summary) - printer.AdvanceRepoProgress() - } - // Process existing repos - for _, name := range scanResult.ManagedFound { - repo := repoMap[name] - result := eng.ProcessRepo(repo) - handleResult(printer, result, &summary) - printer.AdvanceRepoProgress() - } + if *cloneOnlyFlag { + // Clone-only mode: only clone missing repos, skip everything else + printer.StartRepoProgress(len(scanResult.ManagedMissing)) - printer.FinishRepoProgress() + for _, name := range scanResult.ManagedMissing { + repo := repoMap[name] + result := eng.CloneRepo(repo) + handleResult(printer, result, &summary) + printer.AdvanceRepoProgress() + } - // Report collisions - for _, entry := range scanResult.Collisions { - printer.Collision(entry.Name, entry.Detail) - } + printer.FinishRepoProgress() + } else { + // Default mode: full sync + summary.UnknownFolders = len(scanResult.Unknown) + summary.ExcludedButPresent = len(scanResult.ExcludedButPresent) + summary.Errors = len(scanResult.Collisions) + + repoWorkTotal := len(scanResult.ManagedMissing) + len(scanResult.ManagedFound) + printer.StartRepoProgress(repoWorkTotal) + + // Clone missing repos + for _, name := range scanResult.ManagedMissing { + repo := repoMap[name] + result := eng.CloneRepo(repo) + handleResult(printer, result, &summary) + printer.AdvanceRepoProgress() + } - // Report unknown folders - for _, entry := range scanResult.Unknown { - printer.UnknownFolder(entry.Name) - } + // Process existing repos + for _, name := range scanResult.ManagedFound { + repo := repoMap[name] + result := eng.ProcessRepo(repo) + handleResult(printer, result, &summary) + printer.AdvanceRepoProgress() + } - // Report excluded-but-present - for _, entry := range scanResult.ExcludedButPresent { - printer.ExcludedButPresent(entry.Name) + printer.FinishRepoProgress() + + // Report collisions + for _, entry := range scanResult.Collisions { + printer.Collision(entry.Name, entry.Detail) + } + + // Report unknown folders + for _, entry := range scanResult.Unknown { + printer.UnknownFolder(entry.Name) + } + + // Report excluded-but-present + for _, entry := range scanResult.ExcludedButPresent { + printer.ExcludedButPresent(entry.Name) + } } // Print summary