Contributing
Setup
git clone git@github.com:Dxsk/repos-manager.git
cd repos-manager No build step needed - it's pure Bash.
Running tests
# Install bats
sudo pacman -S bats # Arch
brew install bats-core # macOS
# Run all tests
bats tests/*.bats Test coverage
The exact per-module counts drift as features land. Run bats tests/*.bats to see the current total and consult the bats files directly for the up-to-date list of cases. At time of writing the suite is ~90 tests covering CLI routing, flag parsing, pattern matching, config loading (including check_updates and scan_network_mounts), provider URL extraction and the Forgejo credentials helper, lockfile contention, status scanning (including the network-mount pruning and the vendored-directory prune), sync_repo paths and the background update-check module.
Bash portability notes
repos-manager targets bash ≥ 4.0 at runtime, but the CI matrix runs the bats suite on both Ubuntu (bash 5) and macOS (bash 3.2, the system default). That asymmetry catches a handful of old-parser gotchas that have bitten the project. Keep them in mind when touching lib/:
$(expr)inside awk scripts breaks sourcing. Bash 3.2 scans for$(greedily, even inside single-quoted strings, and an awk expression like$(i+1)inside aawk '...'here-script is mis-parsed as an unclosed shell command substitution. The visible symptom isbad substitution: no closing ')' in <(at the function declaration level, which makes every test using that file fail on macOS. Fix: hoist the field index into a plain awk variable first (j = i + 1; $j). Seelib/status.shinside_status_network_mount_pointsfor a commented example.- Unset-array expansion.
"${arr[@]}"aborts underset -uwhenarris empty on bash 3.2. Use${arr[@]+"${arr[@]}"}without outer quotes; the outer quoting wraps the empty expansion into a literal empty word, which on bash 3.2 passes an extra""argument to the following command. log_*helpers andset -e. A barereturnafter a predicate propagates the predicate's exit status, solog_debugreturned 1 whenVERBOSE=falseand killed any caller underset -e. Alwaysreturn 0explicitly when shortcutting out of a log helper.sort -zdefeats streaming.find ... | sort -zforcessortto read every entry before emitting the first line, which silently turned the status progress indicator into a multi-minute hang on large trees. Rely onfind's natural order when streaming matters.- Colors on CI.
log.shdisables colors when stdout is not a TTY and whenNO_COLORis set.tests/test_helper.bashexportsNO_COLOR=1so assertions like[[ "$output" =~ "1 clean" ]]do not have to deal with ANSI escapes.
Linting
shellcheck -x repos-manager.sh lib/*.sh sourceme.bash CI pipeline
Every push triggers 4 checks:
| Check | Tool | What it does |
|---|---|---|
| Bash lint | ShellCheck | Static analysis of all .sh files |
| Tests (Ubuntu) | Bats on bash 5 | Runs the full bats suite |
| Tests (macOS) | Bats on bash 3.2 | Same suite on Apple's legacy bash, catches old-parser regressions |
| Links | Lychee | Validates URLs in all markdown files |
| Shell compat | zsh + fish | Syntax checks on sourceme files |
All checks must pass before merging.
Adding a provider
The provider system is modular. Each provider is a single file in lib/ implementing 4 functions:
Step 1 - Create the provider file
Create lib/yourprovider.sh:
#!/usr/bin/env bash
# YourProvider (uses yourcli)
yourprovider_login() {
yourcli auth login
}
yourprovider_list_repos() {
# Must return a JSON array of objects with:
# nameWithOwner, sshUrl, url
yourcli repo list --json ...
}
yourprovider_get_clone_url() {
local repo_json="$1"
if $USE_HTTPS; then
echo "$repo_json" | jq -r '.url'
else
echo "$repo_json" | jq -r '.sshUrl'
fi
}
yourprovider_get_full_name() {
echo "$1" | jq -r '.nameWithOwner'
} Step 2 - Register it
In repos-manager.sh:
- Source it:
source "${REPOS_MANAGER_LIB}/yourprovider.sh" - Add to
VALID_PROVIDERS - Add to
detect_providers() - Add to
cmd_sync()host mapping - Add to
cmd_sync_all()providers array - Add case in
main()
Step 3 - Add tests
In tests/providers.bats:
@test "yourprovider: get ssh clone url" {
USE_HTTPS=false
local json='{"nameWithOwner":"user/repo","sshUrl":"git@host:user/repo.git","url":"https://host/user/repo"}'
local result
result=$(yourprovider_get_clone_url "$json")
[[ "$result" == "git@host:user/repo.git" ]]
} Step 4 - Document
Update readme.md, site/src/docs/providers.md, and the glossary.
Project structure
$ tree repos-manager/
repos-manager/
├── repos-manager.sh # Entry point, CLI routing
├── Makefile # Install/uninstall
├── flake.nix # Nix flake (optional)
├── sourceme.bash # Bash shell integration
├── sourceme.zsh # Zsh shell integration
├── sourceme.fish # Fish shell integration
├── lib/
│ ├── log.sh # Colors, log functions
│ ├── flags.sh # Flag parsing
│ ├── config.sh # Config file, sourceme gen
│ ├── match.sh # Pattern matching, filter/ignore
│ ├── sync.sh # Core sync engine (parallel)
│ ├── status.sh # Status command (network-mount aware)
│ ├── update.sh # Interactive self-update
│ ├── update_check.sh # Background update check + banner
│ ├── github.sh # GitHub provider
│ ├── gitlab.sh # GitLab provider
│ ├── forgejo.sh # Forgejo/Gitea provider
│ ├── bitbucket.sh # Bitbucket provider
│ └── radicle.sh # Radicle provider
└── tests/
├── test_helper.bash # Shared test utilities
├── cli.bats # CLI tests
├── flags.bats # Flag parsing tests
├── match.bats # Pattern matching tests
├── config.bats # Config tests
├── providers.bats # Provider URL + forgejo creds tests
├── status.bats # Status tests (incl. mount pruning)
├── sync.bats # Sync tests
├── lockfile.bats # Lockfile tests
└── update_check.bats # Update-check banner tests Pull requests
| Rule | Details |
|---|---|
| Branch from | develop |
| Scope | One feature per PR |
| Tests | Must pass (bats tests/*.bats) |
| Lint | Must pass (shellcheck -x) |
| Commits | Conventional style (feat:, fix:, docs:, test:) |