The following video demonstrates the installation, initial sync, and key features of git-sync-all in under 4 minutes.
Topics covered in the video:
git-sync-all is a standalone Bash CLI tool that synchronizes all Git repositories found in a directory tree. Instead of manually switching into each project folder and running git pull, git push, or git commit, git-sync-all does it all in a single command across all your repositories at once.
The tool was built for developers who work on multiple machines and need a reliable, automated way to keep all their local repositories in sync with their remotes β without sacrificing control over what gets committed.
--prune-tags)The tool is non-destructive by default: it never force-pushes, never rebases published history, and always asks before auto-committing unless you explicitly pass --yes.
Always use
--dry-runfirst when running git-sync-all in a new environment. This shows exactly what would happen without making any changes.
--prune-tags support (requires Git 2.17+)--dry-run) for safe previewing--status) renders a table without touching anythingrepos.yml defines expected repos per machine/group. --verify checks completeness, shows clone commands for missing repos, and detects repos on disk not listed in the inventory.--issues shows open GitHub issues per inventory group. Requires gh CLI.~/.config/git-sync-all/config.conf)git check alias integration for even shorter invocation| Requirement | Minimum Version | Notes |
|---|---|---|
| Bash | 4.0+ | macOS ships Bash 3.2 β install via Homebrew: brew install bash |
| Git | 2.17+ | Required for --prune-tags support; older versions still work with a warning |
| make | any | Only required for make install / make link |
| ShellCheck | any | Only required for development (make lint) |
| shfmt | any | Only required for development (make format) |
GitHub CLI (gh) |
any | Optional β only required for --issues. Install from https://cli.github.com/ |
On macOS, the system Bash is version 3.2 (due to GPLv3 licensing). You must install a newer version:
brew install bashand either run the script with/usr/local/bin/bash git-sync-allor update your PATH.
Creates a symlink to the cloned repository. Updates via git pull take effect immediately β no reinstallation needed:
git clone https://github.com/markus-michalski/git-sync-all.git
cd git-sync-all
make link PREFIX=$HOME/.local
Make sure ~/.local/bin is in your PATH. Add to ~/.bashrc or ~/.zshrc if needed:
export PATH="$HOME/.local/bin:$PATH"
The script resolves symlinks at runtime via _gsa_resolve_dir(), so it automatically finds the lib/ directory in the repository β no copied libs needed.
Verify the installation:
git-sync-all --version
# git-sync-all v1.0.0
Install to /usr/local/bin β available for all users. Files are copied, so you must re-run make install after each git pull:
git clone https://github.com/markus-michalski/git-sync-all.git
cd git-sync-all
sudo make install
This runs install -m 755 bin/git-sync-all /usr/local/bin/git-sync-all and copies the library files to /usr/local/lib/git-sync-all/. The Makefile patches the GSA_LIB_DIR path in the installed binary automatically.
With a copy-based installation, you must re-run
sudo make installafter eachgit pullfor changes to take effect. For automatic updates, usemake linkinstead.
Same as system-wide, but installs to ~/.local instead of /usr/local:
git clone https://github.com/markus-michalski/git-sync-all.git
cd git-sync-all
make install PREFIX=$HOME/.local
Same caveat: after
git pull, re-runmake install PREFIX=$HOME/.local. For automatic updates, usemake linkinstead.
After any installation method, register a git check alias for a shorter invocation:
git-sync-all --setup-alias
# Added 'git check' alias to ~/.gitconfig
You can then run:
git check
# Equivalent to: git-sync-all
The alias is stored in ~/.gitconfig as:
[alias]
check = !git-sync-all
git-sync-all --init-config
This creates ~/.config/git-sync-all/config.conf from the bundled example and opens it in your $EDITOR. If no editor is set, it falls back to nano.
# User-local (symlink or copy)
make uninstall PREFIX=$HOME/.local
# System-wide
sudo make uninstall
# Remove git alias
git config --global --unset alias.check
git-sync-all [OPTIONS] [DIRECTORY...]
Without arguments, git-sync-all scans the directory configured as SYNC_BASE_DIRS (default: ~/projekte). You can override the scan target by passing one or more directories as positional arguments:
# Scan default configured directory
git-sync-all
# Scan specific directories
git-sync-all ~/work ~/personal ~/opensource
# Scan a single directory with options
git-sync-all --dry-run ~/work
| Option | Short | Description |
|---|---|---|
--help |
-h |
Show help text and exit |
--version |
-V |
Show version number and exit |
--dry-run |
-n |
Preview all actions without making any changes |
--verbose |
-v |
Increase verbosity level (stackable: -vv for debug) |
--quiet |
-q |
Suppress all output except errors (verbosity 0) |
--yes |
-y |
Auto-confirm all prompts (CI/cron-friendly) |
--config FILE |
-c |
Use a specific config file instead of the default |
--init-config |
Create default config at XDG location and open in $EDITOR |
|
--setup-alias |
Add git check alias to ~/.gitconfig |
|
--no-pull |
Skip pulling from remote | |
--no-push |
Skip pushing to remote | |
--no-tags |
Skip tag synchronization | |
--no-commit |
Skip auto-committing uncommitted changes | |
--no-color |
Disable colored output | |
--status |
Show a status table of all repos without syncing | |
--verify |
Verify inventory: find missing repos and offer removal of unlisted repos | |
--issues |
Show open GitHub issues for inventory repos | |
--inventory FILE |
Use specific inventory file (default: XDG path) | |
--group NAME |
Verify only repos in this group; repeatable. Without --group, falls back to SYNC_VERIFY_GROUP (default: all) |
|
--init-inventory |
Create inventory file at XDG location | |
--exclude PATTERN |
Exclude repos matching the glob pattern (repeatable) | |
--include PATTERN |
Only sync repos matching the glob pattern (repeatable) |
| Level | Flag | Description |
|---|---|---|
| 0 | -q / --quiet |
Errors only |
| 1 | (default) | Normal output: info, warnings, results |
| 2 | -v / --verbose |
Everything including debug messages |
| 3+ | -vv |
Stacks with each additional -v |
Sync all repos interactively at the end of a work session. The tool will ask for confirmation before committing each dirty repository:
git-sync-all
Example output:
[INFO] git-sync-all v1.0.0
[INFO] Scanning: /home/markus/projekte
[INFO] Found 12 repositories
[INFO] my-api-project (main)
[INFO] Uncommitted changes detected
M src/Controller/UserController.php
M src/Service/AuthService.php
? docs/new-endpoint.md
... and 1 more files
Commit and push changes? [y/n/q]: y
[INFO] Committing changes...
[OK] Changes committed
[INFO] Pushing to remote...
[OK] Pushed successfully
[OK] Synced
[INFO] invoice-management (main)
[OK] Clean (nothing to sync)
...
========================================
Synchronization Complete
========================================
Statistics:
Total repositories: 12
Clean (no changes): 9
Synced: 2
Skipped: 1
Failed: 0
[OK] All repositories synchronized successfully!
See exactly what would happen without making any changes. Safe to run at any time:
git-sync-all --dry-run --verbose
Output for a dirty repository in dry-run mode:
[INFO] git-sync-all v1.0.0
[WARN] DRY-RUN mode (no changes will be made)
[INFO] Found 12 repositories
[INFO] my-api-project (main)
[INFO] Uncommitted changes detected
M src/Controller/UserController.php
[DRY-RUN] Would: git add -A
[DRY-RUN] Would: git commit -m "chore: auto-sync from devbox"
[DRY-RUN] Would: git push origin main
Fully automated sync without any user prompts. Add --yes to auto-confirm all repositories and --quiet to suppress output (only errors are printed):
# In crontab: sync every 2 hours during work hours
0 8,10,12,14,16,18 * * 1-5 git-sync-all --yes --quiet
# Or verbose for a logfile
git-sync-all --yes 2>> /var/log/git-sync-all.log
Set in config for permanent automation:
# ~/.config/git-sync-all/config.conf
SYNC_AUTO_CONFIRM=true
SYNC_VERBOSITY=0
Only sync a subset of repositories:
# Only sync repos named "my-project" or "other-project"
git-sync-all --include my-project --include other-project
# Skip repos named "vendor", "node_modules", or anything starting with "test-"
git-sync-all --exclude vendor --exclude node_modules --exclude "test-*"
# Combine: only include "api-*" repos, but not "api-legacy"
git-sync-all --include "api-*" --exclude api-legacy
Include and exclude filters match against the repository directory name (basename), not the full path. Glob patterns are supported:
*,?,[abc].
Get a quick overview of all repository states without making any changes:
git-sync-all --status
Output:
REPOSITORY BRANCH DIRTY UNPUSHED UNPULLED
--------------------------------------------------------------------------------
claude-mcp-osticket main -- -- --
git-sync-all main 3 -- --
invoice-management main -- 2 --
markus-michalski-autor main -- -- --
mlm-gallery main -- -- --
osticket-api-endpoints main -- -- --
[INFO] 6 repositories, 1 with uncommitted changes
Pull updates from all remotes without committing or pushing local changes:
git-sync-all --no-commit --no-push
This is useful when starting work on a machine that may be behind β it only pulls remote changes without touching any local uncommitted work.
git-sync-all follows the XDG Base Directory Specification:
${XDG_CONFIG_HOME}/git-sync-all/config.conf
If XDG_CONFIG_HOME is not set (the common case), this resolves to:
~/.config/git-sync-all/config.conf
Create the config file:
git-sync-all --init-config
Or copy from the bundled example:
cp /usr/local/lib/git-sync-all/../config/config.conf.example \
~/.config/git-sync-all/config.conf
| Variable | Default | Description |
|---|---|---|
SYNC_BASE_DIRS |
$HOME/projekte |
Directories to scan, colon-separated. Example: "$HOME/work:$HOME/personal" |
SYNC_SCAN_DEPTH |
3 |
How many directory levels deep to search for .git directories. 1 = only direct children |
SYNC_EXCLUDE |
(empty) | Colon-separated glob patterns of repo names to skip. Example: "vendor:node_modules:.cache" |
SYNC_INCLUDE |
(empty) | Colon-separated glob patterns. If set, ONLY matching repos are synced |
SYNC_COMMIT_MSG |
chore: auto-sync from {hostname} |
Commit message template. Placeholders: {hostname}, {date}, {repo} |
SYNC_COMMIT_BODY |
Automatic synchronization of uncommitted changes. |
Extended commit body (after blank line) |
SYNC_AUTO_CONFIRM |
false |
Skip all prompts. Same as --yes. Use true for CI/cron |
SYNC_PULL_STRATEGY |
rebase |
How to pull: rebase (clean history) or merge (preserves exact history) |
SYNC_TAGS |
true |
Fetch and prune tags from remote |
SYNC_REMOTE |
origin |
Remote name to sync with |
SYNC_COLOR |
auto |
Color output: auto (detect terminal), true (always), false (never) |
SYNC_VERBOSITY |
1 |
0 = quiet, 1 = normal, 2 = verbose/debug |
SYNC_INVENTORY_FILE |
(XDG default) | Path to inventory file (repos.yml) |
SYNC_VERIFY_GROUP |
all |
Default group for --verify and --issues when --group is not specified. Comma-separated for multiple groups (e.g. "public,private") |
# ~/.config/git-sync-all/config.conf
# Directories to scan for Git repositories (colon-separated)
SYNC_BASE_DIRS="$HOME/projekte:$HOME/work"
# Maximum depth for repo discovery (3 = scan up to 3 levels deep)
SYNC_SCAN_DEPTH=3
# Exclude repos by name (colon-separated glob patterns)
SYNC_EXCLUDE="node_modules:vendor:.cache:__pycache__"
# Only sync these repos (leave empty to sync all)
# SYNC_INCLUDE="my-project:other-project"
# Commit message template
# Available placeholders: {hostname}, {date}, {repo}
SYNC_COMMIT_MSG="chore: auto-sync from {hostname}"
SYNC_COMMIT_BODY="Automatic synchronization of uncommitted changes."
# Auto-confirm without prompts (set true for cron jobs)
SYNC_AUTO_CONFIRM=false
# Pull strategy: rebase (default) or merge
SYNC_PULL_STRATEGY="rebase"
# Sync tags from remote
SYNC_TAGS=true
# Remote name
SYNC_REMOTE="origin"
# Color output: auto, true, false
SYNC_COLOR="auto"
# Verbosity: 0=quiet, 1=normal, 2=verbose
SYNC_VERBOSITY=1
# Default group for --verify/--issues (comma-separated for multiple)
# SYNC_VERIFY_GROUP="all"
The SYNC_COMMIT_MSG supports three placeholders that are replaced at runtime:
| Placeholder | Replaced with | Example |
|---|---|---|
{hostname} |
Output of hostname command |
devbox, macbook-pro |
{date} |
Current date in YYYY-MM-DD format |
2026-02-24 |
{repo} |
Repository directory name (basename) | my-api-project |
Example with all placeholders:
SYNC_COMMIT_MSG="chore: auto-sync {repo} from {hostname} on {date}"
# Result: chore: auto-sync my-api-project from devbox on 2026-02-24
Settings are resolved in this order (higher = wins):
CLI flags > Environment variables > Config file > Built-in defaults
This means you can set permanent defaults in the config file and override them on-the-fly:
# Config has SYNC_AUTO_CONFIRM=false (interactive mode)
# But for this one run, auto-confirm everything:
git-sync-all --yes
# Config has SYNC_VERBOSITY=1, but temporarily go quiet:
SYNC_VERBOSITY=0 git-sync-all
# Use a completely different config for a project:
git-sync-all --config ~/.config/git-sync-all/work.conf
The repository inventory is a YAML file that defines which repositories should exist on a machine. It is useful for ensuring all projects are cloned after a fresh OS install, on a new machine, or when switching between work contexts.
The inventory file lives at:
~/.config/git-sync-all/repos.yml
More precisely: ${XDG_CONFIG_HOME:-$HOME/.config}/git-sync-all/repos.yml. You can override this path with --inventory FILE or the SYNC_INVENTORY_FILE config variable.
git-sync-all --init-inventory
This does one of two things:
repos.yml.example exists, it copies it to the XDG location and opens it in your editor.all group.The format is intentionally simple β flat lists under named group headers:
# Repositories expected on all machines
all:
- git-sync-all
- dotfiles
- script_collection
# Work-specific repositories
work:
- shopware6-sepa-67
- shopware6-facebook-pixel-67
- oxid-module-gallery
# Personal projects
personal:
- my-website
- notes
Each entry is a directory name (not a URL or full path). The verify logic searches all SYNC_BASE_DIRS for a directory with that name containing a .git folder.
For third-party repos that are not in your GitHub account, you can add a clone URL:
external:
- osticket: https://github.com/osTicket/osTicket
- oxid7: https://github.com/OXID-eSales/oxideshop_ce
Both formats coexist β - reponame for your own repos and - reponame: https://url for external ones.
When
--verifyfinds missing repos, it shows concrete clone commands:git clone <url>for repos with URL,gh repo clone <user>/<name>for your own repos.
Group names are freely definable β use whatever makes sense for your workflow (e.g.
all,work,personal,client-abc,server). The groupallis simply the default when--groupis omitted; it has no special meaning beyond that convention. You can change the default group viaSYNC_VERIFY_GROUPin your config (comma-separated for multiple groups).
Verify that every repo listed under the all group exists locally:
git-sync-all --verify
Output:
[OK] git-sync-all β /home/markus/projekte/git-sync-all
[OK] dotfiles β /home/markus/projekte/dotfiles
[ERROR] script_collection β NOT FOUND
2/3 found, 1 missing
[INFO] Missing repositories:
script_collection
gh repo clone markus-michalski/script_collection ~/projekte/script_collection
[INFO] Tip: Add clone URLs in repos.yml for external repos (name: https://...)
Verify only the repos in the work group:
git-sync-all --verify --group work
Verify repos across multiple groups (comma-separated):
git-sync-all --verify --group all,work
Point to a different inventory file (e.g., a shared team inventory):
git-sync-all --verify --inventory ~/Dropbox/team-repos.yml
Verify also detects repos on disk that are not listed in any group of your repos.yml:
git-sync-all --verify
If unlisted repos are found, you are prompted for each one:
[WARN] 2 repo(s) on disk but NOT in inventory:
old-experiment β /home/markus/projekte/old-experiment
temp-fork β /home/markus/projekte/temp-fork
Remove old-experiment from disk? (/home/markus/projekte/old-experiment) [y/n/q]: n
[INFO] Kept: old-experiment
Remove temp-fork from disk? (/home/markus/projekte/temp-fork) [y/n/q]: y
[OK] Removed: temp-fork
[OK] 1 repo(s) removed, 1 kept
Safety: The
--yesflag is ignored for deletion β each repo must be confirmed individually. In--dry-runmode, nothing is deleted. In non-interactive mode (pipe/cron), unlisted repos are only listed, not offered for deletion.
Unlisted repo detection always considers all groups in
repos.yml, regardless of the--groupfilter. This prevents repos from other groups being falsely flagged as unlisted.
Query open GitHub issues for all repositories listed in your inventory. This uses the gh CLI to fetch issue data directly from GitHub.
# Show open issues for all inventory repos
git-sync-all --issues
# Only a specific group
git-sync-all --issues --group public
# With issue details (numbers and titles)
git-sync-all --issues --group public -v
Example output:
REPOSITORY OPEN ISSUES
--------------------------------------------------
git-sync-all 3
dotfiles --
shopware6-sepa-67 1
invoice-management not on GitHub
script_collection not found
[WARN] 2 of 5 repos have open issues (total: 4)
With -v (verbose), individual issues are listed below each repo:
git-sync-all 3
#12 Add --issues flag for GitHub issue queries
#9 Support for multiple remotes
#7 Windows compatibility
The
ghCLI must be installed and authenticated for this feature to work. Install from https://cli.github.com/ and rungh auth loginto authenticate.
git-sync-all was designed for developers who work on the same codebase across multiple machines (e.g., work desktop, home laptop, remote server). The typical workflow is:
End of session (machine A) β git-sync-all β commits + pushes everything
Start of session (machine B) β git-sync-all β pulls all updates
End of session (machine B) β git-sync-all β commits + pushes back
Machine A (work) and Machine B (home) both have:
# ~/.config/git-sync-all/config.conf
SYNC_BASE_DIRS="$HOME/projekte"
SYNC_PULL_STRATEGY="rebase"
SYNC_COMMIT_MSG="chore: auto-sync from {hostname}"
SYNC_AUTO_CONFIRM=false
Workflow:
# End of work session on machine A:
git-sync-all
# β Commits any dirty repos (with your confirmation)
# β Pushes everything to GitHub/GitLab
# Arriving at machine B:
git-sync-all --verify # Check all expected repos are cloned
git-sync-all # Pull all changes from work
# Or just run it normally β it will only push if there's something to push:
git-sync-all
The
{hostname}placeholder in commit messages makes it easy to see which machine auto-committed when reviewing the git log.
For fully automated sync without interaction, use SYNC_AUTO_CONFIRM=true and add to crontab:
# Edit crontab:
crontab -e
# Sync every 2 hours on weekdays, log to file:
0 8,10,12,14,16,18 * * 1-5 /usr/local/bin/git-sync-all --yes --quiet 2>> $HOME/.local/log/git-sync.log
Symptom:
[ERROR] Another instance is running (PID 12345). Remove /tmp/git-sync-all.lock if stale.
Check:
# Is that PID actually running?
ps -p 12345
# Where is the lock file?
ls -la /tmp/git-sync-all.lock
# or
ls -la "${XDG_RUNTIME_DIR:-/tmp}/git-sync-all.lock"
Solution:
If the PID is no longer running (crashed or killed), the tool normally removes the stale lock automatically. If it does not (e.g., the file is owned by another user), remove it manually:
rm /tmp/git-sync-all.lock
If XDG_RUNTIME_DIR is set, the lock file is there instead of /tmp.
Symptom:
A repository you expect to see is not shown in the output.
Check:
Does the repo have a configured remote?
cd ~/projekte/my-repo
git remote get-url origin
# If this fails: no remote β repo is excluded
Is the repo within the scan depth?
# If SYNC_SCAN_DEPTH=3, the .git folder must be at most 3 levels deep
# ~/projekte/level1/level2/level3/my-repo/.git β found
# ~/projekte/level1/level2/level3/level4/my-repo/.git β NOT found
Does the repo match an exclude pattern?
git-sync-all --verbose 2>&1 | grep "Skipping"
Solution:
Increase scan depth or add the parent directory to SYNC_BASE_DIRS:
# Config
SYNC_BASE_DIRS="$HOME/projekte:$HOME/projekte/deeply-nested-group"
SYNC_SCAN_DEPTH=4
Symptom:
[ERROR] Pull failed (possible conflicts - resolve manually)
What happened:
git-sync-all tried to pull (with rebase or merge) but Git detected a conflict that cannot be resolved automatically.
Solution:
Navigate to the failing repository:
cd ~/projekte/my-repo
git status
# Shows conflicting files
Resolve the conflict manually:
# For rebase conflicts:
git rebase --abort # cancel and start fresh, OR
git rebase --continue # after fixing conflicts
# For merge conflicts:
git merge --abort # cancel, OR
# fix files, then:
git add .
git commit
Run git-sync-all again:
git-sync-all --include my-repo
The
--no-commitflag is useful when you want to pull first and inspect conflicts manually before committing your own changes.
Symptom:
/usr/bin/env: bash: version 3.x detected, requires 4.0+
or syntax errors with associative arrays.
Check:
bash --version
# GNU bash, version 3.2.57(1)-release
Solution:
# Install newer Bash via Homebrew
brew install bash
# Verify
/usr/local/bin/bash --version
# GNU bash, version 5.x.x
# Option 1: Run directly with new Bash
/usr/local/bin/bash /usr/local/bin/git-sync-all
# Option 2: Update PATH so 'bash' resolves to new version
echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
Symptom:
[WARN] Git version 2.15 detected. Recommend >= 2.17 for --prune-tags support.
Impact:
Tag synchronization (git fetch --tags --prune-tags) is not supported before Git 2.17. Everything else works fine.
Solution:
Update Git:
# macOS
brew upgrade git
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install git
# Verify
git --version
Symptom:
[ERROR] Config error: directory not found: /home/markus/work (SYNC_BASE_DIRS)
Check:
The directory listed in SYNC_BASE_DIRS does not exist on this machine.
Solution:
Use machine-specific configs or conditionally set the variable:
# Option 1: Use a machine-specific config
git-sync-all --config ~/.config/git-sync-all/home.conf
# Option 2: Set via environment variable (overrides config)
SYNC_BASE_DIRS="$HOME/projects" git-sync-all
# Option 3: Create the missing directory
mkdir -p ~/work
git-sync-all is organized as a main entry point that sources eight library files. Each library is guarded against double-sourcing and focuses on a single concern.
git-sync-all/
βββ bin/
β βββ git-sync-all # Main entry point (~90 lines)
βββ lib/
β βββ core.sh # Strict mode, colors, logging, cleanup, lock file
β βββ config.sh # Config loading, defaults, validation
β βββ cli.sh # Argument parsing, help, version, alias setup
β βββ repo-discovery.sh # Repository scanning, include/exclude filters
β βββ git-ops.sh # All Git operations (subshell-isolated, dry-run aware)
β βββ inventory.sh # YAML inventory parser, verify logic
β βββ issues.sh # GitHub issues check via gh CLI
β βββ sync.sh # Sync workflow, statistics, status table, confirmation
βββ config/
β βββ config.conf.example # Annotated example config
β βββ repos.yml.example # Example repository inventory
βββ tests/
βββ run-tests.sh # Test runner
βββ test-helpers.sh # assert_eq, assert_contains helpers
βββ test-cli.sh # CLI argument parsing tests
βββ test-config.sh # Config loading and validation tests
βββ test-repo-discovery.sh # Repo discovery and filter tests
βββ test-git-ops.sh # Git operation tests
βββ test-inventory.sh # Inventory parsing and verify tests
| Module | Responsibility | Key Functions |
|---|---|---|
bin/git-sync-all |
Orchestration only. Sources all libs, calls main(). Resolves symlinks to find lib dir. |
main(), _gsa_resolve_dir() |
lib/core.sh |
Strict mode (set -euo pipefail), color variables, logging (log_info, log_error, etc.), cleanup trap, dependency check, lock file management |
setup_colors(), check_dependencies(), acquire_lock(), die() |
lib/config.sh |
XDG path resolution, loading config via source, defaults, validation of all variables |
load_config(), _set_defaults(), validate_config(), init_config() |
lib/cli.sh |
while case argument parser, CLI override variables, help/version output, alias setup |
parse_args(), apply_cli_overrides(), show_help(), setup_aliases() |
lib/repo-discovery.sh |
find traversal for .git dirs, remote existence check, include/exclude glob matching |
discover_repos(), _should_skip_repo(), _has_remote() |
lib/git-ops.sh |
All Git operations in subshells for isolation. git_cmd() wrapper respects dry-run mode. Read-only ops always execute; write ops are wrapped. |
git_cmd(), has_uncommitted_changes(), commit_changes(), pull_changes(), push_commits(), sync_tags() |
lib/inventory.sh |
YAML parser for repos.yml, group logic, verify function, unlisted repo detection, inventory initialization (~340 lines) |
parse_inventory(), verify_inventory(), list_inventory_groups(), init_inventory() |
lib/issues.sh |
GitHub issues query via gh CLI, table output, verbose details |
show_issues(), _query_issues(), _get_github_repo(), _find_repo_path() |
lib/sync.sh |
Per-repo sync workflow, user confirmation loop (y/n/q), commit message building, statistics accumulation, status table, summary output |
sync_repository(), sync_all(), show_status(), ask_confirmation(), print_summary() |
Subshell isolation for Git operations
Every function in git-ops.sh runs inside a subshell ( cd "$repo_path" || return 1; ... ). This means:
cd inside a subshell does not kill the entire scriptgit_cmd() dry-run wrapper
Instead of checking DRY_RUN in every single Git call, all write operations go through git_cmd():
git_cmd() {
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log_dry "git $*"
return 0
fi
git "$@"
}
Read-only operations (git status, git log, git rev-parse) always execute β they never modify state, so dry-run mode does not suppress them.
Config is sourced, not parsed
The config file is a shell script that is sourced directly. This means:
$HOME, $(hostname))Lock file uses PID, not flock
The lock file stores the PID of the running process. On startup, if a lock exists, the tool checks whether that PID is still alive with kill -0 "$pid". If not alive, the stale lock is removed and execution continues. This approach works without flock and survives system crashes (the PID will not be running after reboot).
The test suite runs 129 tests across 7 test files:
# Run all tests
make test
# Or directly
bash tests/run-tests.sh
The CI pipeline (GitHub Actions) runs on every push to main and every pull request:
.sh files| Exit Code | Meaning |
|---|---|
0 |
All repositories synchronized successfully (or nothing to sync) |
1 |
One or more repositories failed to sync |
The exit code is determined by the failed counter in the statistics. A repository is counted as failed if:
A repository that is skipped by the user (answering n at the confirmation prompt) is counted as skipped, not failed, and does not contribute to a non-zero exit code.
Can I run git-sync-all in a cron job?
Yes. Use --yes (or SYNC_AUTO_CONFIRM=true in config) to disable all prompts. Use --quiet to suppress output. The exit code is non-zero if any repo fails, so you can hook it into monitoring:
git-sync-all --yes --quiet || notify-send "git-sync-all failed"
What happens if I have a merge conflict?
git-sync-all detects the pull failure and marks that repository as failed. It prints an error message and moves on to the next repository. You need to resolve the conflict manually in that repo and run git-sync-all again (or just git rebase --continue / git merge --continue).
Does it handle repos without a remote?
No. Repos without a configured remote (specifically, without the remote named SYNC_REMOTE, default origin) are silently skipped during discovery. They will not appear in the output at all. This is intentional β git-sync-all is only useful for repos that have a remote to sync with.
Can I use it with GitLab, Bitbucket, or self-hosted Git servers?
Yes. git-sync-all uses standard git fetch, git pull, and git push commands. It works with any remote that your local Git is configured to access, regardless of the hosting provider. SSH keys, HTTPS credentials, and credential helpers all work normally.
What does "rebase" pull strategy mean vs "merge"?
git pull --rebase origin main.git pull origin main.For most developers, rebase produces a cleaner, easier-to-read history.
Can I sync only specific repos without editing the config?
Yes, use CLI flags that override the config for that run only:
# Only sync "my-project" and "other-project"
git-sync-all --include my-project --include other-project
# Skip "legacy-app"
git-sync-all --exclude legacy-app
# Scan a completely different directory
git-sync-all ~/work/client-projects
How do I add a second base directory?
Either pass it as a positional argument (one-time):
git-sync-all ~/projekte ~/work
Or add it permanently to the config (colon-separated):
SYNC_BASE_DIRS="$HOME/projekte:$HOME/work"
Is it safe to run while I am actively editing files?
Yes, as long as you answer n at the confirmation prompt for dirty repos. The tool will never auto-commit without your confirmation unless SYNC_AUTO_CONFIRM=true or --yes is set. Use --dry-run to preview, or --no-commit to skip the commit step entirely.
What is the repository inventory?
A YAML file (repos.yml) that defines which repos should exist on a machine. Use --verify to check if all listed repos are actually present. Useful for ensuring all projects are cloned on a new machine or after a fresh OS install. See the Repository Inventory section for details.
Since v1.3.0, --verify also detects repos on disk that are not listed in the inventory and offers to remove them.
Since v1.4.0, repos.yml supports optional clone URLs for external repos (- name: https://url). Missing repos are shown with concrete clone commands.
git-sync-all --verify --group work
# Set default group via config (instead of "all")
# In config.conf:
# SYNC_VERIFY_GROUP="public,private"
How can I see open GitHub issues for my repos?
Use --issues to query open issues for all repos in your inventory. Add -v for issue numbers and titles. Requires gh CLI installed and authenticated.
git-sync-all --issues
git-sync-all --issues --group work -v
License: MIT β see LICENSE
Links: