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
- Emit `Change::Type { new_mode }` instead of `Change::ExecutableBit` when the base mode is not a regular file.
- Before calling `apply`, check `matches!(mode, Mode::FILE | Mode::FILE_EXECUTABLE)`; otherwise compute the target mode directly.
- Fix the change-classification code so executable-bit changes are only derived from blob entries.
- 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
- Only emit `Change::ExecutableBit` for regular-file entries.
- Use `Change::Type { new_mode }` for symlink, gitlink, or directory mode transitions.
- Unit-test mode transitions against all `Mode` variants.
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
- visit_non_tree() called us
- at least one chunk
- Cannot use iter_v1() on index of type
- Cannot use iter_v2() on index of type
- Tried to use as tree, but was
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)