Sync & rebase
PABLO keeps two things up to date for every project: your task branches, rebased onto the primary branch, and the project's main checkout, fast-forwarded to the remote. Both are cautious: they report by default, skip anything with local changes, and never resolve a conflict in code.
Task branches#
On each project's sync.interval_minutes (or when you run pablo sync:run), PABLO lists the repository's worktrees and syncs only those that belong to an active task. Other worktrees are left alone. For each one:
- fetch, and bring in the branch's own remote first, so a collaborator's push is never overwritten;
- rebase or merge (per
sync.strategy) onto<primary_remote>/<primary_branch>. For a stacked pull request, an open pull request targeting another branch, it uses that base branch instead, whatever the tracker. A merged or closed pull request's base is ignored, and a base that no longer exists falls back to the primary branch; - if the history changed and the branch exists on the remote, push with
--force-with-lease, never plain--force. If the lease fails (someone pushed in the meantime), the worktree is rolled back and sync tries again next time.
Report first, apply when you're ready#
By default sync is a dry run: it reports what it would do. It applies changes only with sync.auto_apply: true in the project (or the global config), or when you run pablo sync:run --apply. Turn auto_apply on for the projects where you know the shape of the work.
pablo sync:run # every project, dry run
pablo sync:run acme-pim --apply
pablo sync:log # what the last sync did, per projectSkipped on purpose#
- ✋ Dirty worktrees, with uncommitted changes, are never rebased.
- 🔒 Busy tasks: sync and the poller never work on the same task at the same time. A locked task is skipped and retried next time.
Conflicts#
PABLO's code never resolves a conflict. When an applied sync hits one, it aborts the rebase cleanly, records the worktree, the branch, the conflicting files and a summary of the hunks, then launches the rebase-conflict-resolver agent headless. The agent replays the rebase onto the exact target sync used, resolves the conflicts and pushes with --force-with-lease; if it can't resolve something safely, it stops without pushing.
The last sync of each project is shown by pablo sync:log and on the dashboard, including the command to open the resolver's session. Each sync replaces the previous log: there's no history, only what the last one did.
Primary checkout update#
On each project's main_update.interval_minutes (default 60, or when you run pablo sync:main), PABLO brings the project's main checkout (repo.path, not the task worktrees) up to date with <primary_remote>/<primary_branch>. Turn it off for a project with main_update.enabled: false.
The checks run in this order; the first one that matches decides:
| Situation | Outcome |
|---|---|
repo.path is missing or isn't a git checkout | ⚠️ missing, skipped |
| a rebase, merge, cherry-pick, revert or bisect is in progress | ✋ busy, skipped |
any change in git status, tracked or untracked, on any branch | ✋ dirty, skipped |
git fetch fails, or the remote branch doesn't exist | ❌ the job fails and is retried next time |
| the local primary branch has commits that aren't on the remote | ✋ ahead, skipped: local commits are never thrown away |
| clean, on another branch, while the primary branch is checked out in another worktree | ⚠️ checked-out-elsewhere, skipped |
| clean, on another branch or a detached HEAD | git checkout <primary_branch> (created from the remote if needed), then the next row |
| clean, on the primary branch | git reset --hard <primary_remote>/<primary_branch>: ✅ updated or up-to-date |
Because a branch with local commits is never touched, the reset --hard is in practice a fast-forward. Task worktrees aren't affected: they always start from the remote primary branch, never from your local one.
pablo sync:main # every project with main_update enabled
pablo sync:main acme-pim # this one, even if main_update is disabled