GitoxideLabs/gitoxide · error

invalid mode change: can't flip executable bit of

Error message

invalid mode change: can't flip executable bit of {mode:?}

What it means

`gix_index::entry::mode::Change::apply()` flips the executable bit of an index entry mode. The library panics because flipping the executable bit is only defined for regular files (`Mode::FILE` / `Mode::FILE_EXECUTABLE`); applying `Change::ExecutableBit` to a symlink, gitlink (submodule), or directory mode is an invalid mode change that the API cannot express.

Solutions

  1. Emit `Change::Type { new_mode }` instead of `Change::ExecutableBit` when the base mode is not a regular file.
  2. Before calling `apply`, check `matches!(mode, Mode::FILE | Mode::FILE_EXECUTABLE)`; otherwise compute the target mode directly.
  3. Fix the change-classification code so executable-bit changes are only derived from blob entries.
  4. If you only need the resulting mode, skip `apply` entirely and construct the desired `Mode` literal.

Example fix

// before
let new_mode = change.apply(mode); // panics for symlinks/gitlinks

// after
let new_mode = match change {
    Change::ExecutableBit if matches!(mode, Mode::FILE | Mode::FILE_EXECUTABLE) => change.apply(mode),
    Change::ExecutableBit => mode, // exec bit meaningless for symlink/gitlink
    Change::Type { new_mode } => new_mode,
};
Defensive patterns

Strategy: validation

Validate before calling

if !matches!(mode, Mode::FILE | Mode::FILE_EXECUTABLE) {
    // ExecutableBit is undefined for symlinks/gitlinks; use a Type change instead.
    return Err(anyhow::anyhow!("exec-bit change invalid for mode {mode:?}"));
}
let new_mode = change.apply(mode);

Type guard

fn exec_bit_applicable(mode: Mode) -> bool {
    matches!(mode, Mode::FILE | Mode::FILE_EXECUTABLE)
}

Try / catch

// This is a panic, not a caught error — avoid it via validation:
let new_mode = if exec_bit_applicable(mode) { change.apply(mode) } else { mode };

Prevention

When it happens

Trigger: Constructing a diff/status change list that emits `Change::ExecutableBit` for a mode that is not `Mode::FILE` or `Mode::FILE_EXECUTABLE` and then calling `change.apply(mode)`. Typically a bug in code that computes `Change` values from tree-vs-index comparisons without classifying the entry type first.

Common situations: Writing custom index/diff logic that treats all entries uniformly and toggles the exec bit on submodule (`COMMIT`) or symlink (`SYMLINK`) entries; upstream changes where a submodule's mode changed; hand-built mode transitions applied to the wrong entry kind.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/411de6a9edd18f42. Report an issue: GitHub.

Appendix: source

Thrown at gix-index/src/entry/mode.rs:135

pub enum Change {
    /// The type of mode changed, like symlink => file.
    Type {
        /// The mode representing the new index type.
        new_mode: Mode,
    },
    /// The executable permission of this file has changed.
    ExecutableBit,
}

impl Change {
    /// Applies this change to `mode` and returns the changed one.
    pub fn apply(self, mode: Mode) -> Mode {
        match self {
            Change::Type { new_mode } => new_mode,
            Change::ExecutableBit => match mode {
                Mode::FILE => Mode::FILE_EXECUTABLE,
                Mode::FILE_EXECUTABLE => Mode::FILE,
                _ => unreachable!("invalid mode change: can't flip executable bit of {mode:?}"),
            },
        }
    }
}

View on GitHub (pinned to e73179060b)