Skip to main content

grit_lib/index/
mode_policy.rs

1//! Mapping worktree file modes onto index modes (`core.filemode`, `core.symlinks`).
2
3use crate::config::ConfigSet;
4
5use super::{MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK};
6
7/// What the worktree filesystem can express about a tracked path's mode.
8///
9/// Git records these capabilities per repository as `core.filemode` (is the executable bit
10/// meaningful?) and `core.symlinks` (can the checkout hold symbolic links?). Both default to
11/// `true`; `git init`/`git clone` write `false` when probing the filesystem fails, which is the
12/// norm on Windows. When a capability is missing, the worktree cannot show that aspect of a
13/// path, so the mode already recorded in the index wins over what `lstat` reports.
14///
15/// Use [`WorktreeModePolicy::index_mode_for_worktree`] whenever a worktree mode is compared
16/// with or written over an index entry: staging, status and worktree diffs all share it.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct WorktreeModePolicy {
19    /// `core.filemode`: the worktree's executable bit is meaningful.
20    pub trust_filemode: bool,
21    /// `core.symlinks`: the worktree can hold symbolic links.
22    pub has_symlinks: bool,
23}
24
25impl Default for WorktreeModePolicy {
26    fn default() -> Self {
27        Self {
28            trust_filemode: true,
29            has_symlinks: true,
30        }
31    }
32}
33
34impl WorktreeModePolicy {
35    /// Read `core.filemode` and `core.symlinks` from `config`, defaulting each to `true` when
36    /// unset or not a boolean.
37    #[must_use]
38    pub fn from_config(config: &ConfigSet) -> Self {
39        let flag = |key: &str| config.get_bool(key).and_then(|r| r.ok()).unwrap_or(true);
40        Self {
41            trust_filemode: flag("core.filemode"),
42            has_symlinks: flag("core.symlinks"),
43        }
44    }
45
46    /// The mode to record for a path given its worktree mode and the mode currently recorded.
47    ///
48    /// Rules, in order:
49    ///
50    /// 1. Without symlink support, a regular file standing in for a recorded symlink
51    ///    ([`MODE_SYMLINK`]) stays a symlink: the file holds the link target.
52    /// 2. Without a trusted executable bit, a regular file over a recorded regular file keeps the
53    ///    recorded `100644`/`100755`.
54    /// 3. Otherwise the worktree mode wins (including real type changes such as a directory, a
55    ///    symlink on a symlink-capable checkout, or a gitlink).
56    ///
57    /// # Parameters
58    ///
59    /// - `worktree_mode` — Git mode derived from `lstat` (see [`crate::diff::mode_from_metadata`]).
60    /// - `recorded_mode` — the index entry's mode, or `None` for a path not yet tracked (the
61    ///   worktree mode is then returned unchanged).
62    ///
63    /// # Returns
64    ///
65    /// The Git mode to compare against or store in the index.
66    #[must_use]
67    pub fn index_mode_for_worktree(self, worktree_mode: u32, recorded_mode: Option<u32>) -> u32 {
68        let Some(recorded) = recorded_mode else {
69            return worktree_mode;
70        };
71        if !is_regular_file_mode(worktree_mode) {
72            return worktree_mode;
73        }
74        if !self.has_symlinks && recorded == MODE_SYMLINK {
75            return MODE_SYMLINK;
76        }
77        if !self.trust_filemode && is_regular_file_mode(recorded) {
78            return recorded;
79        }
80        worktree_mode
81    }
82
83    /// Whether a worktree mode is indistinguishable from `recorded_mode` under this policy.
84    ///
85    /// Equivalent to `index_mode_for_worktree(worktree_mode, Some(recorded_mode)) == recorded_mode`.
86    #[must_use]
87    pub fn worktree_mode_matches(self, worktree_mode: u32, recorded_mode: u32) -> bool {
88        self.index_mode_for_worktree(worktree_mode, Some(recorded_mode)) == recorded_mode
89    }
90}
91
92fn is_regular_file_mode(mode: u32) -> bool {
93    mode == MODE_REGULAR || mode == MODE_EXECUTABLE
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::index::{MODE_GITLINK, MODE_TREE};
100
101    const NO_SYMLINKS: WorktreeModePolicy = WorktreeModePolicy {
102        trust_filemode: true,
103        has_symlinks: false,
104    };
105    const NO_FILEMODE: WorktreeModePolicy = WorktreeModePolicy {
106        trust_filemode: false,
107        has_symlinks: true,
108    };
109
110    #[test]
111    fn default_trusts_worktree() {
112        let p = WorktreeModePolicy::default();
113        assert_eq!(
114            p.index_mode_for_worktree(MODE_REGULAR, Some(MODE_SYMLINK)),
115            MODE_REGULAR
116        );
117        assert_eq!(
118            p.index_mode_for_worktree(MODE_EXECUTABLE, Some(MODE_REGULAR)),
119            MODE_EXECUTABLE
120        );
121        assert!(!p.worktree_mode_matches(MODE_REGULAR, MODE_SYMLINK));
122    }
123
124    #[test]
125    fn no_symlinks_keeps_recorded_symlink_for_regular_file() {
126        for wt in [MODE_REGULAR, MODE_EXECUTABLE] {
127            assert_eq!(
128                NO_SYMLINKS.index_mode_for_worktree(wt, Some(MODE_SYMLINK)),
129                MODE_SYMLINK
130            );
131            assert!(NO_SYMLINKS.worktree_mode_matches(wt, MODE_SYMLINK));
132        }
133        // The executable bit is still trusted for real regular files.
134        assert_eq!(
135            NO_SYMLINKS.index_mode_for_worktree(MODE_EXECUTABLE, Some(MODE_REGULAR)),
136            MODE_EXECUTABLE
137        );
138        // A real symlink on disk is still a symlink.
139        assert_eq!(
140            NO_SYMLINKS.index_mode_for_worktree(MODE_SYMLINK, Some(MODE_REGULAR)),
141            MODE_SYMLINK
142        );
143    }
144
145    #[test]
146    fn no_filemode_keeps_recorded_regular_mode() {
147        assert_eq!(
148            NO_FILEMODE.index_mode_for_worktree(MODE_EXECUTABLE, Some(MODE_REGULAR)),
149            MODE_REGULAR
150        );
151        assert_eq!(
152            NO_FILEMODE.index_mode_for_worktree(MODE_REGULAR, Some(MODE_EXECUTABLE)),
153            MODE_EXECUTABLE
154        );
155        // Symlink support is trusted: a file over a symlink entry is a type change.
156        assert_eq!(
157            NO_FILEMODE.index_mode_for_worktree(MODE_REGULAR, Some(MODE_SYMLINK)),
158            MODE_REGULAR
159        );
160        assert_eq!(
161            NO_FILEMODE.index_mode_for_worktree(MODE_REGULAR, Some(MODE_GITLINK)),
162            MODE_REGULAR
163        );
164    }
165
166    #[test]
167    fn untracked_and_non_regular_worktree_modes_pass_through() {
168        let none = WorktreeModePolicy {
169            trust_filemode: false,
170            has_symlinks: false,
171        };
172        assert_eq!(
173            none.index_mode_for_worktree(MODE_EXECUTABLE, None),
174            MODE_EXECUTABLE
175        );
176        assert_eq!(
177            none.index_mode_for_worktree(MODE_TREE, Some(MODE_SYMLINK)),
178            MODE_TREE
179        );
180        assert_eq!(
181            none.index_mode_for_worktree(MODE_GITLINK, Some(MODE_REGULAR)),
182            MODE_GITLINK
183        );
184    }
185
186    #[test]
187    fn from_config_reads_both_keys() {
188        let mut cfg = ConfigSet::new();
189        assert_eq!(
190            WorktreeModePolicy::from_config(&cfg),
191            WorktreeModePolicy::default()
192        );
193        cfg.add_command_override("core.symlinks", "false")
194            .expect("set symlinks");
195        cfg.add_command_override("core.filemode", "false")
196            .expect("set filemode");
197        assert_eq!(
198            WorktreeModePolicy::from_config(&cfg),
199            WorktreeModePolicy {
200                trust_filemode: false,
201                has_symlinks: false,
202            }
203        );
204    }
205}