gitbutlerapp/gitbutler · error

cannot mix mark sources

Error message

cannot mix mark sources

What it means

The status TUI's mark store (crates/but/src/command/legacy/status/tui/app/mark.rs:367) is a single-kind selection enum (Empty/Hunks/Commits/CommittedFiles/Branches). insert_mark for uncommitted hunks/files bails with 'cannot mix mark sources' when the store already holds a different kind. This is an internal invariant guarding the TUI's one-kind-at-a-time marking model, not a user configuration error.

Source

Thrown at crates/but/src/command/legacy/status/tui/app/mark.rs:367

    }
}

impl MarkStore<UncommittedHunkOrFile> for Marks {
    type Error = anyhow::Error;

    fn contains_mark(&self, mark: &UncommittedHunkOrFile) -> bool {
        self.as_hunks()
            .is_some_and(|hunks| hunks.iter().any(|hunk| hunk == mark))
    }

    fn insert_mark(&mut self, mark: UncommittedHunkOrFile) -> Result<(), Self::Error> {
        if self.contains_mark(&mark) {
            return Ok(());
        }
        match self {
            Self::Empty => *self = Self::Hunks(NonEmpty::new(mark)),
            Self::Hunks(hunks) => hunks.push(mark),
            _ => anyhow::bail!("cannot mix mark sources"),
        }
        Ok(())
    }

    fn remove_mark(&mut self, mark: &UncommittedHunkOrFile) {
        let Self::Hunks(hunks) = self else {
            return;
        };

        if remove_from_non_empty(hunks, |marked| marked == mark) {
            *self = Self::Empty;
        }
    }
}

impl MarkStore<CommitId> for Marks {
    type Error = anyhow::Error;

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Clear existing marks (reset to Empty) before inserting marks of a different kind.
  2. At the input layer, branch on the current kind (as_hunks()) and reject or reset mixed selections before insert_mark is reached.
  3. Write a TUI test reproducing the mixed-kind key sequence and fix the handler that permits it.

Example fix

// before
marks.insert_mark(UncommittedHunkOrFile::File(f))?; // may hit Hunks-vs-other mix

// after
if !matches!(marks, Marks::Empty | Marks::Hunks(_)) {
    marks = Marks::Empty; // one kind at a time
}
marks.insert_mark(UncommittedHunkOrFile::File(f))?;
Defensive patterns

Strategy: validation

Validate before calling

if !matches!(marks, Marks::Empty | Marks::Hunks(_)) {
    marks = Marks::Empty; // enforce one mark kind per session
}
marks.insert_mark(hunk_or_file)?;

Type guard

fn accepts_hunk_marks(marks: &Marks) -> bool {
    matches!(marks, Marks::Empty | Marks::Hunks(_))
}

Try / catch

if let Err(err) = marks.insert_mark(mark) {
    if err.to_string().contains("cannot mix mark sources") {
        marks = Marks::Empty;
        marks.insert_mark(mark)?; // retry with a fresh session
    } else { return Err(err); }
}

Prevention

When it happens

Trigger: TUI code calling insert_mark(UncommittedHunkOrFile) while marks of another variant (commits, committed files, branches) are active; a key sequence or new feature that lets the user mark a hunk and then a branch without clearing marks first.

Common situations: Developers extending TUI mark handling; regressions after refactoring mark modes; keybinding handlers that switch selection context without resetting marks.

Related errors


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