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 a awk '...' here-script is mis-parsed as an unclosed shell command substitution. The visible symptom is bad 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). See lib/status.sh inside _status_network_mount_points for a commented example.
  • Unset-array expansion. "${arr[@]}" aborts under set -u when arr is 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 and set -e. A bare return after a predicate propagates the predicate's exit status, so log_debug returned 1 when VERBOSE=false and killed any caller under set -e. Always return 0 explicitly when shortcutting out of a log helper.
  • sort -z defeats streaming. find ... | sort -z forces sort to read every entry before emitting the first line, which silently turned the status progress indicator into a multi-minute hang on large trees. Rely on find's natural order when streaming matters.
  • Colors on CI. log.sh disables colors when stdout is not a TTY and when NO_COLOR is set. tests/test_helper.bash exports NO_COLOR=1 so 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:

  1. Source it: source "${REPOS_MANAGER_LIB}/yourprovider.sh"
  2. Add to VALID_PROVIDERS
  3. Add to detect_providers()
  4. Add to cmd_sync() host mapping
  5. Add to cmd_sync_all() providers array
  6. 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:)