Browse docs
Docs / Guides / Branches & Merging
View as Markdown

Branches & Merging

Swarmfile branches follow the Perforce-streams model, not git branches. A branch is a full working line over the project's content-addressed storage, not a lightweight pointer into a shared history graph - but because storage blocks are content-addressed and shared, forking a branch doesn't copy any file data. Only the metadata diverges, and only once you actually change something. Fork a 4TB project and the fork costs you nothing until you start editing.

Branches are project-scoped#

A branch applies to an entire project, not a folder inside it. You don't branch a subdirectory and leave the rest of the project on main - every mount of the project works one branch, in full, independently of every other mount. This keeps the model simple: there's never a question of which branch a given folder is "really" on.

Creating, listing, and archiving#

swarmfile branch create look-dev-warm --from main
swarmfile branch list
swarmfile branch archive look-dev-warm

branch create forks a new branch, defaulting to a fork from the branch this mount is currently working on if you omit --from (so from inside a branch, branch create forks that branch - use --from main to fork main explicitly). branch list shows the active branches on the project. branch archive soft-archives a branch you're done with - the project's default branch (normally main) can't be archived.

A branch create (or a merge) is refused while a file on the source branch is still uploading, so it never silently takes that file's previous version. Retry once the upload finishes. If the uploading machine crashed, see Troubleshooting.

Pruning merged branches#

After a batch of branches has landed, branch merged lists the ones with nothing left to bring over - a branch-vs-base diff with no changed files and no conflicts (the same computation swarmfile diff <branch> shows, so a branch whose diff can't be computed is left out rather than assumed merged). branch prune archives all of them in one pass, after naming them and asking; --yes skips the prompt for scripts.

swarmfile branch merged
swarmfile branch prune

prune never archives a branch an open mount on this machine is working on - if you're checked out to a merged branch, or another mount here is on one, it's skipped (and merged marks it). A protected branch is listed, but archiving it is refused unless you're an org admin (exit 2). (A teammate's mount on another machine isn't visible to this check.)

"Merged" here means merged into its own base (the branch the diff is computed against), not specifically into main - the diff route only compares a branch to its base, so there's no --into <branch> form.

All three operations are also available from:

  • The web dashboard - the Commits view lists, creates, and archives branches, and can trigger a merge; the Branches tab (see Protecting many branches at once) shows every branch's protection status plus protection rules. Both of those dashboard tabs are admin-gated.
  • The Desktop App - a Branches panel does the same, applied immediately, with no restart required for list, create, or archive.
  • The CLI - swarmfile branch {list,create,switch,archive,merged,prune,protect,unprotect}, swarmfile protection-rule {list,create,delete,add-reviewer,remove-reviewer,list-reviewers}, swarmfile tag {list,create,delete}, swarmfile merge, swarmfile mr {create,list,status,approve,request-changes,review-comment,merge,ready,close,reopen,comment,comments,assign,unassign,request-review,unrequest-review,label,unlabel}, and swarmfile label {list,create,update,delete}.

A typical use: fork a branch to try a look-dev variant or a design option without duplicating the whole project, then merge it back if it works out, or archive it if it doesn't.

Branching from a past commit, or a tag#

By default, branch create forks the source branch as it is right now. Give it a commit ref instead (any of the forms below), and it forks the source exactly as it stood at that point - every file carrying the content it had then, in one call, instead of forking at head and then rolling the fork back with checkout:

swarmfile branch create recover --from-commit 482
swarmfile branch create recover --from-tag v1.0

--from <REF> takes the full ref vocabulary - a branch, tag, #N/HEAD~N, changeset id, or 64-hex commit hash - so --from main forks another branch, while --from-commit/--from-tag are the explicit seq/tag spellings. --from is optional with either: the targeted commit or tag already names which branch it lived on. If you give --from anyway and it disagrees with that branch, the command refuses rather than silently picking one. The fork keeps the state it started from: files inherited from an older point stay readable even after that commit ages past history retention.

This is the primitive a headless or unattended machine - a render farm node, a CI runner - uses to pin itself to an exact historical state with a single command and no Desktop App or browser interaction:

swarmfile commit -m "submit render job 482"
swarmfile tag create v1.0
swarmfile branch create render-fix --from-tag v1.0

A tag is a project-scoped, immutable name for a commit. It also pins that commit against history retention: while the tag exists, checkout/restore to it keeps working even after the commit would otherwise have been pruned, and the content it names stays stored; deleting the tag releases the pin. There's no "move a tag" command in the swarmfile CLI; to repoint a name, delete it and create it again (with git access, a lightweight tag can also be moved with git push -f origin <tag> - see Cloning a Project with Git - unless a release was published from it):

swarmfile tag list
swarmfile tag create v1.0
swarmfile tag delete v1.0

tag create defaults to the branch's CURRENT head - exactly what the commit above just produced, no need to look up its commit number first. Give it --at-seq <n> explicitly to tag an older commit instead of the head:

swarmfile tag create v0.9 --at-seq 482

The web dashboard's Commits view offers the same two actions per commit row - "Branch from here" and "Tag" - plus a tag picker next to the branch selector; the Desktop App's Branches panel has a source-type picker (branch / commit number / tag) on its create form, and its own Tags panel for list/create/delete.

Switching branches#

swarmfile branch switch look-dev-warm
swarmfile branch switch     # omit the name to switch back to the default branch

Switching branches is hot - no engine restart, no interruption to the mount. The first time a machine visits a branch, switching to it pays a one-time full sync (proportional to that branch's size); a repeat visit to a branch it's already synced is a fast incremental catch-up. Switching is refused while a changelist is open - submit or cancel it first, so staged work is never silently discarded or reattached to the wrong branch. Files open in applications are closed as part of the switch, and their pending changes are saved to the branch you're leaving; an application that keeps writing to such a file afterwards gets an error, and nothing it writes lands on the new branch.

List, create, and archive don't touch the running mount and take effect immediately, same as switch now does.

The web dashboard has a separate, metadata-only branch switcher on the Files tab - a GitHub-repo-browser-style ⎇ main dropdown that scopes which branch's file tree the dashboard is showing you. It never touches a mount: switching it there has no relationship to, and no effect on, which branch any desktop client is actually checked out to. Use it to browse another branch's files from the browser without needing a machine checked out to it.

Merging#

swarmfile merge
swarmfile merge --record-conflicts

merge performs a whole-file three-way merge of your mount's current branch back into the branch it was forked from - its parent. That's usually main, but a branch forked from a non-main branch merges back into that branch. The direction is implicit either way: there's no target flag to set, because a branch already knows its own parent. To land a branch you aren't standing on, name it: swarmfile merge --from feature-x merges feature-x into its own parent without switching mounts. Because these are binary assets, there's no line-level merge to fall back on - merging is winner-take-all per file, with conflict detection when the same file changed on both sides since the fork point. By default, conflicts are surfaced as a list; --record-conflicts instead records conflict rows you can resolve interactively, one file at a time, rather than just being told a file collided. A recorded merge conflict belongs to the branch being merged into, so when you resolve it, local is the incoming branch's version and remote is the version already on the target branch: --winner remote keeps the target's copy. Either way the next merge lands cleanly, because the source branch is updated to match the kept version.

For text conflicts, the engine can try a three-way line merge before you pick a side: swarmfile merge --diff3 batch-resolves every text-file content conflict it can, then retries the merge once. A project can make that the default by committing .swarmfile/merge.yml with diff3: true (see Project-local files); --no-diff3 forces it off for one invocation. Anything it can't merge stays a conflict for the resolution flow below, and the web dashboard's merge button offers the same auto-resolve step when a merge comes back conflicted.

Two more flags help on big or cautious merges. swarmfile merge --dry-run prints exactly what would change - added, modified, deleted, renamed, moved, and conflicting files, the same read-only diff swarmfile diff shows - and stops without staging anything or taking the branch's merge lock. swarmfile merge --wait blocks until a large background apply finishes, printing progress phase by phase (the command otherwise returns as soon as the merge is accepted); it gives up after ten minutes with exit 2. And if the target branch moves after the merge was prepared, the merge refuses with stale_merge instead of overwriting the newer state - run merge again and it lands cleanly.

How this holds up on a very large project#

Branching never copies file content: blocks aren't copied or re-read, only a little metadata per folder, and that metadata is written in parallel. So forking stays fast as a project grows - thousands of folders are proportionally more metadata, not more file bytes - and the first time a new branch is opened or synced, its folders are likewise read in parallel. On a 100,000-entry tree, a fork that creates 120+ directories completes in tens of seconds.

Merging is metadata-only in the same way. A merge larger than the single-pass staging budget continues as a resumable background job: the command accepts it, swarmfile op status shows progress while the staged diff applies, and the target head advances once, when it finishes.

Long-running work no longer depends on the terminal staying open. merge, commit, checkout, revert, branch create/archive, mr merge, trash restore/purge, project delete, materialize, ignore-clean, hydrate free-space and closing a mount run as operations in the engine: the command waits and prints the result as usual, however long it takes. Press Ctrl-C and it detaches instead of stopping - the command exits 3 and prints the operation's id - and --no-wait returns the id at once; swarmfile op list / op status / op wait / op cancel collect the outcome later, even after the engine restarts. (Ctrl-C cannot stop a commit, merge or delete part-way: those complete or fail on their own, and op cancel refuses them.)

Commands not on that list use their own route's socket deadline - five seconds for most, up to three minutes for a few - and if one reports control socket request failed: no answer within …s, it stopped waiting but the work usually continues or is already done. Check swarmfile branch list (or swarmfile mounts list) before retrying: re-running branch create for an existing name answers that the branch already exists, which means the first one succeeded.

Merge Requests#

swarmfile merge lands a branch immediately - useful when you're merging your own work and you're confident it's ready. When you want someone else to look at a branch before it lands, open a Merge Request instead - from the web dashboard's Merge Requests tab, Merge requests in the Desktop App's left rail, or swarmfile mr create --title "...": pick the branch, give it a title, and it's posted for review.

A Merge Request shows reviewers exactly what would change - the same list of added, modified, deleted, renamed, and moved files a direct merge would land, computed fresh every time you open it, so it's never a stale snapshot. Click a changed text file to expand an inline diff right there, with syntax highlighting and word-level highlighting on modified lines; a file that conflicts with the target branch is flagged right in that list too, and clicking it shows a 3-way diff instead - your changes and the target's changes, each compared against the version both branches started from, so you can see exactly what's colliding before resolving it. Reviewers leave an Approve or Request changes verdict (optionally with a note); only the latest verdict from each reviewer counts, so changing your mind is just casting a new one. A verdict is also tied to the source-branch commit it was cast against: pushing a new commit afterwards retires every earlier verdict - approvals and "request changes" alike - so the approval floor has to be met again on the new head. Merge only becomes available once the approval gate is met and nobody's latest verdict is "request changes". On an unprotected target that gate is zero approvals; protecting a branch is what sets a floor, starting at one approval and configurable up to ten (a branch matched by a protection rule uses the rule's count, and when both apply the effective count is the higher of the two). There's still no required-specific-reviewer enforcement - raising the count (below) is the whole gating mechanism, not naming who has to be one of the approvers; requesting a specific reviewer (below) is a nudge, not a requirement - anyone's approval still counts toward the floor, requested or not. Every merge request also carries its own discussion thread - the same kind of comments you'd leave on any file - for review remarks that aren't about one specific line of the diff.

There's also a third, non-blocking Comment verdict (swarmfile mr review-comment <number>) for feedback that isn't a vote either way - it shows up in the review history same as Approve/Request changes, but never counts toward the approval floor and never blocks a merge. Unlike the other two, a Comment is allowed on your own merge request, since it can't be used to rubber-stamp your own work the way a self-approval could.

Merging a Merge Request does exactly what swarmfile merge does - same conflict detection, same all-or-nothing landing, same optional "record conflicts for resolution" fallback (and the same --diff3 auto-resolve override) if the branch drifted since anyone last looked at the diff. It also pins the source branch: if the head moved after the approval gate and CI check ran, the merge refuses with stale_merge rather than landing a commit the checks never saw. A Merge Request that's approved but hits a conflict on merge still 409s, same as a direct merge would; nothing lands until the branch is fixed. Closing one abandons it without merging - nothing about the branch changes - and a closed (never-merged) request can be reopened later if it turns out the work wasn't actually abandoned, from the web dashboard, the Desktop App, or swarmfile mr reopen <number>.

Merging also offers an opt-in delete the source branch checkbox (swarmfile mr merge <number> --delete-branch) - unchecked by default. It only takes effect once the merge itself has actually landed, and archiving is a soft branch archive, never a hard delete; the same protected-branch admin gate applies, so if the source branch got protected in the meantime and you're not an admin, the merge still succeeds but the branch stays as-is - you'll see a note saying so rather than the merge itself failing.

Any project member can open, review, and merge a Merge Request - it isn't admin-gated, unlike RFIs. The one restriction is that a merge request's creator can't review their own - the hub refuses with self_review regardless of which surface asks (web, Desktop App, or CLI); the web dashboard and Desktop App both gray out Approve/Request changes on your own merge request rather than letting you find out only after submitting.

Draft merge requests#

A Merge Request doesn't have to be ready for review the moment it exists. Pass --draft to hold it back:

swarmfile mr create --title "Warmer facade material" --draft
swarmfile mr ready 3

A draft behaves like any other merge request for everything except merging and notifications at creation time: comments, reviews, assigning, requesting a review, and labels all work exactly the same, and assigning someone or requesting their review BY HAND still notifies them even while the request is a draft - it's only the initial "here's a new merge request" fan-out, and an auto-reviewer's request-review notification, that are held back at creation, on the assumption a draft isn't actually ready for anyone's general attention yet. That fan-out isn't deferred to later, either - it's skipped for good: marking a draft ready doesn't retroactively send the notification it held back at creation, so use assignees or requested reviewers if you need to be sure someone specific gets pinged. What's actually gated is merging: mr merge refuses a draft outright, even one that's already picked up an approval. mr ready 3 (also available from the web dashboard and Desktop App detail view) flips it to a normal, reviewable request. There's no way back to draft once it's marked ready.

Assignees, requested reviewers, and labels#

Beyond the approve/reject verdicts above, a Merge Request carries three pieces of bookkeeping - all purely organizational, none of them gating whether mr merge is refused:

  • Assignees - who's on the hook to land it. One or more people, or a group.
  • Requested reviewers - who's been asked to take a look. Distinct from an assignee (who lands it) and distinct from actually having reviewed (the Approve/Request changes verdicts above) - requesting someone is a heads-up, not a requirement, so it never changes who has to approve for mr merge to unlock.
  • Labels - a per-project set of tags an admin defines once (swarmfile label create bug --color "#FF5733"), then anyone can attach to any merge request in that project to filter the list by.

Requested reviewers don't have to be added by hand every time. A branch protection rule can name a set of auto-reviewers - CODEOWNERS-style, per target branch - and every merge request opened against a branch a rule matches gets those reviewers requested automatically the moment it's created. An auto-requested reviewer is attached exactly like a manually-requested one, and only ever adds to the list - it never stops anyone else from requesting a different reviewer by hand. The one difference is notification timing on a draft: a manual request-review always notifies, but an auto-reviewer added at creation time on a draft MR is silently attached with no notification (same reasoning as the "new merge request" fan-out above) - they show up as a requested reviewer either way, just without the ping until the MR is marked ready and something else notifies them. See Protecting many branches at once for how to configure the list.

All three are editable from the web dashboard's detail-view sidebar, or the CLI:

swarmfile mr assign 3 <principal-id>
swarmfile mr unassign 3 <principal-id>
swarmfile mr request-review 3 <principal-id>
swarmfile mr unrequest-review 3 <principal-id>
swarmfile mr label 3 <label-id>
swarmfile mr unlabel 3 <label-id>
swarmfile label list
swarmfile label create bug --color "#FF5733"
swarmfile label update <label-id> --name defect --color "#000000"
swarmfile label delete <label-id>

Assigning, requesting a review, and attaching a label are all idempotent (doing it twice is a no-op, not an error) and don't require anything from the target - there's no accept/decline step, since these are informational, not access grants. Defining, renaming, or deleting a label is admin-gated; attaching an existing label to a merge request isn't.

From the Desktop App#

Merge requests in the left rail opens a list (filterable by status, refreshing automatically as teammates act - and re-checking periodically while it's open) that drills into a detail view for one request: branch names, the changed-file list, the full review history, the discussion thread, and a review composer - click through to Merge or Close from there, both behind the same confirm step every other consequential Desktop App action uses. Same as the web dashboard, click a changed text file for its inline diff, or a conflicting file for a 3-way diff. A + New Merge Request button on the list opens a small inline form (source branch, title, optional description, and a "Create as draft" checkbox) rather than needing the CLI or the browser just to propose a review.

A draft shows a Draft badge in place of the usual approval pill, and its Merge button is replaced by Mark ready for review until you flip it. If a target branch requires CI checks to pass, a merge blocked purely on a failing/missing check says so specifically ("Required checks haven't passed yet") rather than the generic "needs approval" message. Assigning, requesting a review, and labeling are done from the web dashboard or the CLI - the Desktop App shows them read-only.

From the CLI#

swarmfile mr create --title "Warmer facade material"
swarmfile mr list
swarmfile mr status 3
swarmfile mr approve 3
swarmfile mr request-changes 3 --note "needs another pass"
swarmfile mr merge 3
swarmfile mr close 3
swarmfile mr reopen 3
swarmfile mr comment 3 --message "Looks good once the fascia texture is swapped."
swarmfile mr comments 3

mr create's --from defaults to whatever branch the mount is currently on, so a CI job that forked its own branch and did the work can open a review with no separate branch-name bookkeeping. mr status <number> doubles as a CI gate - its exit code is 0 if approved, 2 if open but not yet approved, 1 on any other error. mr comment/mr comments post to and list the same discussion thread the web dashboard and Desktop App show.

mr approve/request-changes/merge/close/reopen put review actions a terminal command away, not just the web dashboard or Desktop App. The hub's self_review gate stops the merge request's own creator from reviewing it; an approval from a second credential counts, so treat review-capable credentials like any other approval authority in your org. If you want an independent-approval guarantee, protect the target branch with --required-approvals set above 1 (below).

Protecting a branch#

swarmfile merge and a direct mr merge both bypass review the moment nobody insists otherwise. Protecting a branch removes that choice for changes arriving as a merge: once set, a bare swarmfile merge into it (or mr merge on a request that hasn't cleared the branch's approval/checks gates) is refused outright with protected_branch - the branch's changes have to arrive through an approved Merge Request.

Protection governs what lands on the branch, whether it arrives as a merge or a direct write. Once set, a bare swarmfile merge (or mr merge on a request that hasn't cleared the branch's gates) is refused, and so is every direct write from a mount: saving, editing, renaming, moving, or deleting a file, creating one, staging/committing directly, rolling back, resolving a conflict, or reflecting LFS content all answer 409 protected_branch. The way through is a branch plus an approved Merge Request; project ACLs still decide who may ask, but they cannot make a protected branch writable.

Before you protect: update every engine first. Protection is enforced by the hub, and the desktop app on each machine decides how to react to a 409 protected_branch refusal. Update the app on every machine that works on a branch before you protect it, so staged work is held for you to move to a branch rather than discarded.

swarmfile branch protect release
swarmfile branch protect release --required-approvals 2
swarmfile branch unprotect release

Setting or clearing protection is admin-gated - a plain project member gets refused (the CLI exits 2, distinguishable from a hard error), same as trying to archive a protected branch. Every protect/unprotect leaves an audit trail: who did it, and when, shows up in the dashboard's Activity feed (owner-visible, same as an ACL-mode change). The web dashboard's Files-tab branch switcher and the Commits toolbar both offer a protect/unprotect toggle for admins, alongside a 🔒 badge everyone sees; the Desktop App's Branches panel has the same toggle.

On a protected branch the mergeability rule is "≥ the branch's effective required-approvals (1-10, default 1), 0 requested-changes"; on an unprotected branch the approval half is 0, so a direct mr merge needs none until the branch is protected. A single changes_requested verdict still blocks a merge outright no matter how many approvals exist, and that part isn't configurable. --required-approvals (1-10) sets the approval half for this one branch; leaving it off keeps the default of 1 while the flag is set. There's still no required-specific-reviewer enforcement - raising the count is the whole gating mechanism, not naming who has to be one of the approvers; requesting a specific reviewer flags whose attention it needs without making their approval individually required.

swarmfile protection-rule create release --require-checks-to-pass

--require-checks-to-pass exists on protection rules only, not on branch protect - to gate one exact branch on CI, use a no-wildcard rule like the one above. It adds a second, independent gate alongside approvals: once set, a merge is refused unless the latest run of every check reported at the merge request's exact current head - not an earlier commit it happened to pass on - is green. A rule that last reported at an earlier commit isn't carried forward as still-required, but if no check has reported at the head at all, the gate fails closed rather than passing vacuously - the same "no news is bad news" convention GitHub's required-status-checks uses, so the gate can't be quietly satisfied by a check nobody's runner has gotten around to yet. This stacks with --required-approvals rather than replacing it - both have to clear - and the merge request detail view exposes checks state separately from approval state, so an MR can visibly be "approved, blocked on checks." A check doesn't have to come from Swarmfile's own runner - any CI system that can call an authenticated API can report one.

Protecting many branches at once#

Protecting one branch by name is fine for main, but not for a release/* line that grows every sprint. A protection rule protects every branch whose name matches a glob pattern - including ones forked after the rule was created, with nothing to re-apply:

swarmfile protection-rule create "release/*" --required-approvals 2
swarmfile protection-rule list
swarmfile protection-rule delete "release/*"

* matches any run of characters, ? matches exactly one; a pattern with no wildcards (protection-rule create main) is just another way to protect one exact branch - equivalent to branch protect main, not a second, different mechanism. A branch counts as protected if its own flag is set OR any rule matches its name, so deleting a rule can un-protect a branch that was never explicitly protected itself. --required-approvals composes the same way: the effective count for a branch is the MAX of its own value (if flag-protected) and every matching rule's - the same "most restrictive wins" resolution the boolean uses, just with the higher number instead of true. Same admin gate, same exit-2 convention, and same audit trail as the per-branch toggle - a rule is arguably the more consequential of the two, since one rule change can flip many branches (including future ones) at once.

The two mechanisms don't override each other: branch unprotect release/1.0 only ever clears that branch's OWN flag. If a rule like release/* still matches it, it stays protected regardless, and every surface says so - the CLI/Desktop App/web response reports the branch's real, effective state after the unprotect, not just "done."

A protection rule is also where auto-reviewers live:

swarmfile protection-rule add-reviewer "release/*" <principal-id>
swarmfile protection-rule remove-reviewer "release/*" <principal-id>
swarmfile protection-rule list-reviewers "release/*"

Every merge request opened against a branch matching release/* requests a review from each configured principal automatically, the moment it's created - no one has to remember to add them by hand. The list is matched against the merge request's target branch, same as the rule's own pattern; it's additive to whatever reviewers get requested manually, never a replacement.

Rules are managed from the CLI or the web dashboard's Branches tab (admin-gated, alongside Commits) - a single view of every branch's protection status plus every active rule, since neither the Files-tab switcher nor the Commits toolbar shows more than one branch at a time. The same Branches tab has an Edit button on any already-protected branch to raise its required-approval count without unprotecting it first; the Desktop App's Branches panel has the same Edit button on its own protected-branch rows, an inline number field replacing the row's usual buttons rather than a separate dialog. Protection rules themselves - creating or deleting a release/*-style pattern - are CLI/web-only; a Desktop App user sees a rule-protected branch's 🔒 badge (with an ×N suffix once the count is above 1) correctly either way, since the hub resolves the effective protection state and count server-side before any client sees them, just can't create or delete a rule from the Desktop App.

Protecting a branch - by flag or by rule - is only as strong as the Merge Request review it forces you through, and it inherits the caveat above: the hub stops an MR's own creator from approving it, while an approval from a second credential counts. --required-approvals above 1 raises the bar - clearing a 2-reviewer rule takes two approval credentials, not one - but no server-side check can prove that two accounts are two different humans without identity verification. If you protect main specifically to guarantee a human looked at every merge into it before it landed, keep review-capable credentials out of any automation that opens merge requests.

A different kind of conflict#

Branch merges aren't the only place conflicts happen - and the other kind is more common. If you and a teammate both edit the same file on the same branch while one of you was offline, Swarmfile catches it when the offline edit tries to land: the change was made against a base version that's no longer current. Rather than silently keeping both copies and leaving you to notice, it records a conflict and refuses further writes to that file until it's resolved. That's why the file shows as read-only on macOS and Linux and an application's save fails with "permission denied" ("access denied" on Windows), and why swarmfile conflicts exists - a filesystem has no way to say "someone else changed this too." The web file browser badges the row with Conflict so it's visible without opening the file, the Desktop App's Conflicts panel lists it with its path (and the branch, when it isn't the one this mount is on), and the CLI is where a terminal sees the reason.

Resolving is a deliberate choice between several outcomes. In the web dashboard and the Desktop App (both open the same picker):

  • Keep mine - your offline version becomes the current head; the other edit is superseded.
  • Keep theirs - keep the version already on the hub. Your offline edit isn't discarded: it's saved in the file's history (as a version owned by you), so you can restore it later with a rollback or from the History view. Choose Keep both instead when you want your version visible alongside the file right away.
  • Keep both - the hub keeps the current head, and your version lands alongside it as a sibling file with a conflict suffix, so nothing is lost and you reconcile them by hand.
  • Try an automatic merge - for file types with a registered merge tool (currently plain text and common source/config formats), click "Try automatic merge" and Swarmfile runs a three-way line merge of your edit against theirs. If it succeeds cleanly, a fourth choice appears - the merged result - for you to review and accept instead of picking a side. If the file type isn't supported, or both edits touched the same lines, the attempt just fails harmlessly and you're left with the three choices above.

From the CLI, swarmfile conflicts resolve <path> covers the first three non-interactively via --winner local|remote|keep-both. --auto runs the same built-in three-way merge the picker's "Try automatic merge" does - no external tool and no git config needed, and it exits 2 when a real conflict remains (so a script can tell "a human must choose" apart from "the engine was unreachable" and fall back); add --all to sweep every conflicted file on this mount's branch in one pass (rows on other branches are counted and skipped unless --include-other-branches is given - pass it or merge --diff3 when landing a merge, whose conflict is recorded under the target branch). --tool [name] is the option the picker doesn't have: it hands the conflict to an external mergetool - kdiff3, Beyond Compare, VS Code's merge editor, anything speaking git's own $BASE/$LOCAL/$REMOTE/$MERGED protocol - auto-discovered from your git config. Unlike the built-in merge, --tool works on any file type, binary included, since the tool does the actual reconciling rather than Swarmfile attempting a line-based diff. See CLI: swarmfile for the full flag reference. --tool is CLI-only by design: an external tool needs a real terminal/GUI session to run in, which only a foreground CLI process has.

Like branch merging, whole-file is always available as a fallback here - a binary asset never has a line-level merge to fall back on, which is why keep-mine/keep-theirs/keep-both always work regardless of file type. Until you resolve the conflict, the file stays read-only; once you do, writes resume with no restart.

An unresolved conflict also stops every other operation that would move that file's content: a branch merge, a point-in-time restore, a commit revert, and a rollback all refuse with conflict_unresolved until it's resolved. Resolve it first, then re-run the operation.

Where to go next#

  • Version Control covers changesets, checkout, and per-file history - the model branches sit on top of.
  • CLI: swarmfile has the full command reference for branch and merge.