gitbutlerapp/gitbutler · error

Invalid source: expected an uncommitted file or branch

Error message

Invalid source: expected an uncommitted file or branch

What it means

`but absorb` folds uncommitted changes into the commits they belong to. Its optional source must be an uncommitted file/hunk ID (absorb just that file) or a branch/lane ID (absorb everything assigned to that lane); no source absorbs everything uncommitted. This bail is the `_` catch-all of the AbsorptionTarget conversion: the resolved CliId was neither `UncommittedHunkOrFile` nor `Branch` (e.g. a commit or another entity kind reached the match).

Source

Thrown at crates/but/src/command/legacy/absorb.rs:89

        })
        .transpose()?;

    let target = if let Some(source) = source {
        match source {
            CliId::UncommittedHunkOrFile(UncommittedHunkOrFile { hunks, .. }) => {
                // Absorb this particular file
                AbsorptionTarget::Hunks {
                    hunks: hunks.map(|id_and_hunk| id_and_hunk.hunk).into(),
                }
            }
            CliId::Branch(branch) => {
                // Absorb everything that is assigned to this lane
                AbsorptionTarget::Branch {
                    branch_name: branch.name,
                }
            }
            _ => {
                anyhow::bail!("Invalid source: expected an uncommitted file or branch");
            }
        }
    } else {
        // Try to absorb everything uncommitted
        Default::default()
    };

    // TODO: Ideally, there's a simpler way of getting the worktree changes without passing the context to it.
    // At this time, the context is passed pretty deep into the function.
    let absorption_plan =
        but_api::legacy::absorb::absorption_plan_with_perm(ctx, target, guard.write_permission())?;

    // Absorbing into a landed commit entangles the new change with an entire
    // already-merged commit, which conflicts on the next `but pull`. Drop such
    // plan entries and tell the user, instead of silently amending them.
    let merged = MergedUpstream::from_ctx(ctx, allow_merged)?;
    let Some((absorption_plan, skipped_merged)) =
        drop_landed_absorptions(absorption_plan, &merged, out)?

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Run `but absorb` with no source to absorb all uncommitted changes.
  2. Pass an uncommitted-file ID or a branch/lane ID from `but status` (`but absorb 2a` for a file, `but absorb b1` for a lane).
  3. If calling absorb programmatically, filter IdMap results to `CliId::UncommittedHunkOrFile` or `CliId::Branch` before converting to AbsorptionTarget.

Example fix

# before
but absorb 4z   # 4z is a commit ID -> Invalid source: expected an uncommitted file or branch

# after
but absorb       # absorb all uncommitted changes
but absorb 2a    # 2a = uncommitted file ID
but absorb b1    # b1 = branch/lane ID
Defensive patterns

Strategy: validation

Validate before calling

# Verify the ID names an uncommitted file or a lane before absorbing
but status | grep -F '<id>' || { echo 'not an absorbable source'; exit 2; }
but absorb '<id>'

Type guard

// For callers of the IdMap / but API: only these variants are absorbable sources
fn is_absorbable_source(clid: &CliId) -> bool {
    matches!(clid, CliId::UncommittedHunkOrFile { .. } | CliId::Branch(_))
}

Prevention

When it happens

Trigger: A resolved CLI ID of an entity kind other than uncommitted file/hunks or branch (e.g. `CliId::Commit`) reaching the AbsorptionTarget match in crates/but/src/command/legacy/absorb.rs:89. Note the handler pre-filters sources at absorb.rs:56-59 and rejects bad selectors with a different message, so this arm fires when an unexpected CliId kind slips through (other callers, future variants).

Common situations: Passing a commit ID copied from `but status` to `but absorb`; scripts invoking the absorb handler with an unvalidated IdMap result; CLI versions drifting so new entity kinds are not covered by the match arms.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/7e7e22788871100c. Report an issue: GitHub.