OXYGENHUB
# eaf3e9068180 oxygen / crates / oxy-core / src / repo / checkout.rs
11.3 KiB · Rust · last changed Never move a subrepo's bookmark backwards from its parent 1 month ago source blame history raw
98286406 Andy McCall ↶ 1
//! Moving the working copy to another state (spec §12.3, §7.7).
2
3
use super::*;
4
5
#[derive(Debug, Default, Clone, PartialEq, Eq)]
6
pub struct CheckoutReport {
7
    pub written: usize,
8
    pub removed: usize,
9
    pub conflicted: bool,
4dee277c Andy McCall ↶ 10
    /// Nested repositories that could not be moved, and other remarks.
11
    pub notes: Vec<String>,
98286406 Andy McCall ↶ 12
}
13
14
impl Repo {
15
    /// Move the working copy to the state whose heads are `heads` (spec §12.3):
16
    /// the working directory is rewritten to that state and a fresh
17
    /// working-copy change is started on it. The bookmark does not move.
75167672 Andy McCall 18
    pub fn checkout(
19
        &self,
20
        heads: &[ChangeHash],
21
        discard: bool,
22
        command: &str,
23
    ) -> Result<CheckoutReport> {
1a3b6203 Andy McCall ↶ 24
        self.checkout_keeping(heads, discard, command, &BTreeSet::new())
25
    }
26
27
    /// [`Repo::checkout`], leaving the nested repositories at
28
    /// `keep_subrepos` as they are (their pins are carried work).
29
    pub(crate) fn checkout_keeping(
30
        &self,
31
        heads: &[ChangeHash],
32
        discard: bool,
33
        command: &str,
34
        keep_subrepos: &BTreeSet<String>,
35
    ) -> Result<CheckoutReport> {
6caacfff Andy McCall ↶ 36
        let _wc = self.lock_working_copy()?;
98286406 Andy McCall ↶ 37
        let snap = self.snapshot(command)?;
38
        if !discard && (!snap.wc.tree_ops.is_empty() || !snap.wc.edits.is_empty()) {
1c2bd9ab Andy McCall ↶ 39
            let list: Vec<String> = snap
40
                .statuses
41
                .iter()
42
                .take(8)
43
                .map(|s| format!("{} {}", status_word(s.status), s.path))
44
                .collect();
45
            return fail(format!(
46
                "the working copy has changes ({}{}); commit them, take them along with --carry, or drop them with --discard",
47
                list.join(", "),
48
                if snap.statuses.len() > 8 { ", …" } else { "" }
49
            ));
98286406 Andy McCall ↶ 50
        }
51
        let target_state = self.load_state(heads)?;
52
        let heads = target_state.maximal(heads);
53
        let target = target_state.materialize();
cdb5beba Andy McCall ↶ 54
        let attrs = Repo::state_attributes(&target);
98286406 Andy McCall ↶ 55
        let current = self.walk_working_copy()?;
56
        let mut report = CheckoutReport::default();
c863c7a7 Andy McCall ↶ 57
        // Paths this file system cannot hold side by side (spec §7.7).
58
        report.notes.extend(
59
            self.platform
60
                .problems(target.files.keys().map(String::as_str)),
61
        );
98286406 Andy McCall ↶ 62
4dee277c Andy McCall ↶ 63
        // Remove tracked paths that the target does not have. A nested
64
        // repository is never deleted (spec §7.8); it is left in place.
98286406 Andy McCall ↶ 65
        let mut dirs_to_prune: BTreeSet<PathBuf> = BTreeSet::new();
4dee277c Andy McCall ↶ 66
        for (path, entry) in &current {
98286406 Andy McCall ↶ 67
            if !target.files.contains_key(path) {
4dee277c Andy McCall ↶ 68
                if entry.kind == NodeKind::Subrepo {
75167672 Andy McCall 69
                    report
70
                        .notes
71
                        .push(format!("{path}: nested repository left in place"));
4dee277c Andy McCall ↶ 72
                    continue;
73
                }
98286406 Andy McCall ↶ 74
                let full = self.safe_path(path)?;
75
                fs::remove_file(&full)?;
76
                report.removed += 1;
77
                if let Some(parent) = full.parent() {
78
                    dirs_to_prune.insert(parent.to_path_buf());
79
                }
80
            }
81
        }
7421309f Andy McCall ↶ 82
        // Write target files that differ (only inside the sparse filter).
98286406 Andy McCall ↶ 83
        for (path, f) in &target.files {
7421309f Andy McCall ↶ 84
            if !self.in_scope(path) {
85
                continue;
86
            }
98286406 Andy McCall ↶ 87
            match &f.content {
88
                FileContent::Dir => continue,
89
                FileContent::Text { bytes, .. } => {
cdb5beba Andy McCall ↶ 90
                    let a = attrs.lookup(path);
75167672 Andy McCall 91
                    let on_disk: std::borrow::Cow<[u8]> =
92
                        if a.eol == Some(crate::attributes::Eol::Crlf) {
93
                            crate::attributes::to_crlf(bytes).into()
94
                        } else {
95
                            bytes.as_slice().into()
96
                        };
97
                    let same = current.get(path).is_some_and(|c| {
c863c7a7 Andy McCall ↶ 98
                        c.kind == NodeKind::File
99
                            && c.content == *on_disk
100
                            && (!self.platform.exec_bits || c.exec == f.mode.exec)
75167672 Andy McCall 101
                    });
98286406 Andy McCall ↶ 102
                    if !same {
cdb5beba Andy McCall ↶ 103
                        self.write_file(path, &on_disk, f.mode.exec)?;
98286406 Andy McCall ↶ 104
                        report.written += 1;
105
                    }
cdb5beba Andy McCall ↶ 106
                    if a.lockable && !self.is_locked(path) {
107
                        let _ = set_readonly(&self.root.join(path), true);
108
                    }
98286406 Andy McCall ↶ 109
                }
110
                FileContent::Opaque { blobs } => {
111
                    let Some(blob) = blobs.first() else { continue };
112
                    let bytes = self.read_blob(blob)?;
4dee277c Andy McCall ↶ 113
                    if f.kind == NodeKind::Subrepo {
114
                        let pin = crate::subrepo::Pin::decode(&bytes)?;
1a3b6203 Andy McCall ↶ 115
                        if keep_subrepos.contains(path) {
116
                            continue;
117
                        }
118
                        let sync = self.sync_subrepo(path, &pin, false)?;
119
                        if matches!(
120
                            sync,
121
                            SubrepoSync::Updated | SubrepoSync::KeptBookmark { .. }
122
                        ) {
123
                            report.written += 1;
4dee277c Andy McCall ↶ 124
                        }
1a3b6203 Andy McCall ↶ 125
                        report.notes.extend(sync.note(path));
4dee277c Andy McCall ↶ 126
                        continue;
127
                    }
98286406 Andy McCall ↶ 128
                    if f.kind == NodeKind::Symlink {
75167672 Andy McCall 129
                        let same = current
130
                            .get(path)
131
                            .is_some_and(|c| c.kind == NodeKind::Symlink && c.content == bytes);
98286406 Andy McCall ↶ 132
                        if !same {
133
                            self.write_symlink(path, &bytes)?;
134
                            report.written += 1;
135
                        }
136
                    } else {
75167672 Andy McCall 137
                        let same = current.get(path).is_some_and(|c| {
c863c7a7 Andy McCall ↶ 138
                            c.kind == NodeKind::File
139
                                && c.content == bytes
140
                                && (!self.platform.exec_bits || c.exec == f.mode.exec)
75167672 Andy McCall 141
                        });
98286406 Andy McCall ↶ 142
                        if !same {
143
                            self.write_file(path, &bytes, f.mode.exec)?;
144
                            report.written += 1;
145
                        }
cdb5beba Andy McCall ↶ 146
                        if attrs.lookup(path).lockable && !self.is_locked(path) {
147
                            let _ = set_readonly(&self.root.join(path), true);
148
                        }
98286406 Andy McCall ↶ 149
                    }
150
                    if blobs.len() > 1 {
151
                        for (i, b) in blobs.iter().enumerate().skip(1) {
75167672 Andy McCall 152
                            self.write_file(
153
                                &format!("{path}.oxy-conflict-{}", i + 1),
154
                                &self.read_blob(b)?,
155
                                f.mode.exec,
156
                            )?;
98286406 Andy McCall ↶ 157
                        }
158
                    }
159
                }
160
            }
161
        }
162
        // Prune directories left empty, deepest first.
163
        for d in dirs_to_prune.iter().rev() {
164
            let mut d = d.clone();
75167672 Andy McCall 165
            while d != self.root
166
                && fs::read_dir(&d)
167
                    .map(|mut i| i.next().is_none())
168
                    .unwrap_or(false)
169
            {
98286406 Andy McCall ↶ 170
                fs::remove_dir(&d)?;
171
                let Some(p) = d.parent() else { break };
172
                d = p.to_path_buf();
173
            }
174
        }
175
176
        let (op_ref, op) = self.head_op()?;
6d3eefbc Andy McCall ↶ 177
        let wc = self.new_working_copy(heads.clone(), "");
98286406 Andy McCall ↶ 178
        let wc_hash = self.store.put_change(&wc)?;
179
        let mut view = op.view.clone();
180
        view.workspaces.insert(self.workspace, wc_hash);
181
        self.record_op(&[op_ref], command, view)?;
182
        report.conflicted = target.is_conflicted();
b7f102fc Andy McCall ↶ 183
        let blobs: HashMap<String, ObjectRef> = target
184
            .files
185
            .iter()
186
            .filter_map(|(p, f)| match &f.content {
75167672 Andy McCall 187
                FileContent::Opaque { blobs } if f.kind == NodeKind::File && blobs.len() == 1 => {
188
                    Some((p.clone(), blobs[0]))
189
                }
b7f102fc Andy McCall ↶ 190
                _ => None,
191
            })
192
            .collect();
696bbf29 Andy McCall ↶ 193
        let entries = self.walk_working_copy_with(None, &Self::tracked_kinds(&target), None)?;
b7f102fc Andy McCall ↶ 194
        self.write_manifest_entries(report.conflicted, false, &entries, &blobs)?;
6d3eefbc Andy McCall ↶ 195
        let _ = self.run_hook(
196
            "post-checkout",
75167672 Andy McCall 197
            &format!(
198
                "{{\"event\":\"post-checkout\",\"heads\":{},\"written\":{},\"removed\":{}}}",
199
                json_array(heads.iter().map(|h| json_str(&h.to_hex()))),
200
                report.written,
201
                report.removed
202
            ),
6d3eefbc Andy McCall ↶ 203
        );
98286406 Andy McCall ↶ 204
        Ok(report)
205
    }
206
207
    /// The absolute path for a repository path, refusing anything that would
208
    /// escape the workspace or write through a symlink (spec §7.7).
209
    fn safe_path(&self, rel: &str) -> Result<PathBuf> {
210
        let mut full = self.root.clone();
211
        let comps: Vec<&str> = rel.split('/').collect();
212
        for (i, comp) in comps.iter().enumerate() {
213
            if comp.is_empty() || *comp == "." || *comp == ".." || comp.contains('\\') {
214
                return fail(format!("refusing unsafe path {rel:?}"));
215
            }
216
            if i == 0 && *comp == OXY_DIR {
217
                return fail(format!("refusing to write inside {OXY_DIR}: {rel:?}"));
218
            }
219
            if i + 1 < comps.len() {
220
                full.push(comp);
221
                if let Ok(m) = fs::symlink_metadata(&full) {
222
                    if m.file_type().is_symlink() {
75167672 Andy McCall 223
                        return fail(format!(
224
                            "refusing to write through symlink {}",
225
                            full.display()
226
                        ));
98286406 Andy McCall ↶ 227
                    }
228
                }
229
            } else {
230
                full.push(comp);
231
            }
232
        }
233
        Ok(full)
234
    }
235
c6aa5bd9 Andy McCall ↶ 236
    pub(crate) fn write_file(&self, rel: &str, bytes: &[u8], exec: bool) -> Result<()> {
98286406 Andy McCall ↶ 237
        let full = self.safe_path(rel)?;
238
        if let Some(parent) = full.parent() {
239
            fs::create_dir_all(parent)?;
240
        }
241
        if fs::symlink_metadata(&full).is_ok_and(|m| m.file_type().is_symlink() || m.is_dir()) {
242
            remove_any(&full)?;
243
        }
cdb5beba Andy McCall ↶ 244
        if fs::metadata(&full).is_ok_and(|m| m.permissions().readonly()) {
245
            set_readonly(&full, false)?;
246
        }
98286406 Andy McCall ↶ 247
        fs::write(&full, bytes)?;
248
        set_executable(&full, exec)?;
249
        Ok(())
250
    }
251
c6aa5bd9 Andy McCall ↶ 252
    pub(crate) fn write_symlink(&self, rel: &str, target: &[u8]) -> Result<()> {
98286406 Andy McCall ↶ 253
        let full = self.safe_path(rel)?;
254
        if let Some(parent) = full.parent() {
255
            fs::create_dir_all(parent)?;
256
        }
257
        if fs::symlink_metadata(&full).is_ok() {
258
            remove_any(&full)?;
259
        }
260
        #[cfg(unix)]
261
        {
262
            std::os::unix::fs::symlink(String::from_utf8_lossy(target).as_ref(), &full)?;
263
            Ok(())
264
        }
265
        #[cfg(not(unix))]
266
        {
267
            // Spec §7.7: without symlink support, write the target as a plain file.
268
            fs::write(&full, target)?;
269
            Ok(())
270
        }
1c2bd9ab Andy McCall ↶ 271
    }
272
}
273
274
/// How a refusal names a path's change: "removed" is a file of the working
275
/// copy's base that is missing on disk.
276
fn status_word(s: ChangeStatus) -> &'static str {
277
    match s {
278
        ChangeStatus::Added => "added",
279
        ChangeStatus::Modified => "modified",
280
        ChangeStatus::Removed => "removed",
281
        ChangeStatus::Renamed => "renamed",
282
        ChangeStatus::ModeChanged => "mode changed",
283
        ChangeStatus::ConflictEdited => "conflict edited",
284
        ChangeStatus::Resolved => "resolved",
98286406 Andy McCall ↶ 285
    }
286
}