OXYGENHUB
◆ v0.2.0 oxygen / crates / oxy-core / src / repo / bookmarks.rs
21.3 KiB · Rust · last changed Remote bookmarks work like local ones; a newer format names the oxy that cannot read it (closes #48, closes #49) 1 month ago source blame history raw
7b9809e6 Andy McCall ↶ 1
//! Bookmarks (spec §10.1): named states, any number per repository, with a
2
//! current bookmark per workspace. Merging is the union of two bookmarks'
3
//! heads: no merge change, conflicts reified.
4
5
use super::*;
6
7
/// One bookmark as `oxy bookmark list` shows it.
8
#[derive(Clone, Debug)]
9
pub struct BookmarkInfo {
10
    pub name: String,
11
    pub heads: Vec<ChangeHash>,
12
    /// The workspace's current bookmark.
13
    pub current: bool,
14
    /// Changes ahead of / behind `origin`'s copy, when tracked.
15
    pub ahead: Option<usize>,
16
    pub behind: Option<usize>,
17
}
18
19
#[derive(Clone, Debug)]
20
pub struct MergeReport {
21
    pub heads: Vec<ChangeHash>,
22
    pub changed: bool,
23
    pub checked_out: bool,
24
}
25
26
/// Bookmark and tag names (spec §10.1): UTF-8, 1–255 bytes, no control
27
/// characters, no leading `-`, and none of `..`, `//`, `@{`.
28
pub fn valid_reference_name(name: &str) -> bool {
29
    !name.is_empty()
30
        && name.len() <= 255
31
        && !name.starts_with('-')
32
        && !name.chars().any(|c| c.is_control())
33
        && !name.contains("..")
34
        && !name.contains("//")
35
        && !name.contains("@{")
36
        && name != "policy"
37
}
38
14edbaaf Andy McCall ↶ 39
/// The remote bookmark `spec` names as `REMOTE/NAME`, if `view` has one:
40
/// the remote, the bookmark's name there, and its state. The longest
41
/// matching remote name wins, and a bookmark name may hold `/` itself.
42
pub fn remote_bookmark<'a>(view: &'a View, spec: &str) -> Option<(&'a str, &'a str, ObjectRef)> {
43
    view.remotes
44
        .iter()
45
        .filter_map(|(remote, bookmarks)| {
46
            let rest = spec.strip_prefix(remote.as_str())?.strip_prefix('/')?;
47
            let (name, state) = bookmarks.get_key_value(rest)?;
48
            Some((remote.as_str(), name.as_str(), *state))
49
        })
50
        .max_by_key(|(remote, _, _)| remote.len())
51
}
52
53
/// A bookmark a remote has and this repository has no bookmark for.
54
#[derive(Clone, Debug)]
55
pub struct RemoteOnlyBookmark {
56
    pub remote: String,
57
    pub name: String,
58
    pub heads: Vec<ChangeHash>,
59
}
60
4fe7d604 Andy McCall ↶ 61
/// What one state lacks of another: see [`Repo::state_gap`].
62
#[derive(Debug, Clone, PartialEq, Eq)]
63
pub struct StateGap {
64
    /// Changes of the first state recorded before the latest head of the
65
    /// second but not in it, oldest first: what Git's ancestry would have
66
    /// included.
67
    pub earlier: Vec<ChangeHash>,
68
    /// How many changes of the first state the second lacks altogether.
69
    pub lacks: usize,
70
    /// The heads of the second state together with `earlier`: everything
71
    /// of the first state up to the second's latest head.
72
    pub upto: Vec<ChangeHash>,
73
}
74
7b9809e6 Andy McCall ↶ 75
impl Repo {
14edbaaf Andy McCall ↶ 76
    pub(crate) fn workspace_bookmark_path(&self) -> PathBuf {
75167672 Andy McCall 77
        self.oxy
78
            .join("workspaces")
79
            .join(self.workspace.to_hex())
80
            .join("bookmark")
7b9809e6 Andy McCall ↶ 81
    }
82
83
    /// Every bookmark with its heads and remote-tracking difference.
84
    pub fn bookmarks(&self) -> Result<Vec<BookmarkInfo>> {
85
        let view = self.view()?;
86
        let mut out = Vec::new();
87
        for (name, st) in &view.bookmarks {
88
            let heads = self.store.get_state(st)?.heads;
89
            let (ahead, behind) = match view.remotes.get("origin").and_then(|b| b.get(name)) {
90
                Some(rs) => {
91
                    let remote_heads = self.store.get_state(rs)?.heads;
75167672 Andy McCall 92
                    (
93
                        self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok(),
94
                        self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok(),
95
                    )
7b9809e6 Andy McCall ↶ 96
                }
97
                None => (None, None),
98
            };
75167672 Andy McCall 99
            out.push(BookmarkInfo {
100
                name: name.clone(),
101
                heads,
102
                current: *name == self.config.bookmark,
103
                ahead,
104
                behind,
105
            });
7b9809e6 Andy McCall ↶ 106
        }
107
        Ok(out)
14edbaaf Andy McCall ↶ 108
    }
109
110
    /// Bookmarks of remotes with no local bookmark of the same name, by
111
    /// remote then name.
112
    pub fn remote_only_bookmarks(&self) -> Result<Vec<RemoteOnlyBookmark>> {
113
        let view = self.view()?;
114
        let mut out = Vec::new();
115
        for (remote, bookmarks) in &view.remotes {
116
            for (name, st) in bookmarks {
117
                if view.bookmarks.contains_key(name) {
118
                    continue;
119
                }
120
                out.push(RemoteOnlyBookmark {
121
                    remote: remote.clone(),
122
                    name: name.clone(),
123
                    heads: self.store.get_state(st)?.heads,
124
                });
125
            }
126
        }
127
        Ok(out)
128
    }
129
130
    /// The local bookmark to switch to for `spec`, creating it from a
131
    /// remote's when there is none (as `git switch` does): `NAME` takes
132
    /// `origin/NAME` (or the one remote that has it), and `REMOTE/NAME`
133
    /// takes that one. Returns the local name and, when it was just
134
    /// created, the remote it came from. A local bookmark tracks the
135
    /// remote's of the same name: `oxy pull` updates it.
136
    pub fn local_bookmark_for(&self, spec: &str) -> Result<(String, Option<String>)> {
137
        let (op_ref, op) = self.head_op()?;
138
        if op.view.bookmarks.contains_key(spec) {
139
            return Ok((spec.to_string(), None));
140
        }
141
        let (remote, name, state) = match remote_bookmark(&op.view, spec) {
142
            Some((remote, name, state)) => (remote.to_string(), name.to_string(), state),
143
            None => {
144
                let mut having: Vec<&String> = op
145
                    .view
146
                    .remotes
147
                    .iter()
148
                    .filter(|(_, b)| b.contains_key(spec))
149
                    .map(|(r, _)| r)
150
                    .collect();
151
                if let Some(i) = having.iter().position(|r| *r == "origin") {
152
                    having = vec![having[i]];
153
                }
154
                match having.as_slice() {
155
                    [] => return fail(format!("no bookmark {spec}, here or on a remote")),
156
                    [r] => {
157
                        let state = op.view.remotes[*r][spec];
158
                        (r.to_string(), spec.to_string(), state)
159
                    }
160
                    several => {
161
                        let names: Vec<String> =
162
                            several.iter().map(|r| format!("{r}/{spec}")).collect();
163
                        return fail(format!(
164
                            "several remotes have {spec}: name one, as {}",
165
                            names.join(" or ")
166
                        ));
167
                    }
168
                }
169
            }
170
        };
171
        if op.view.bookmarks.contains_key(&name) {
172
            // REMOTE/NAME where NAME is already local: switch to the local one.
173
            return Ok((name, None));
174
        }
175
        let mut view = op.view.clone();
176
        view.bookmarks.insert(name.clone(), state);
177
        self.record_op(
178
            &[op_ref],
179
            &format!("oxy bookmark track {remote}/{name}"),
180
            view,
181
        )?;
182
        Ok((name, Some(remote)))
7b9809e6 Andy McCall ↶ 183
    }
184
185
    /// Create `name` at `heads` (default: the working copy's base heads).
75167672 Andy McCall 186
    pub fn bookmark_create(
187
        &self,
188
        name: &str,
189
        heads: Option<&[ChangeHash]>,
190
    ) -> Result<Vec<ChangeHash>> {
7b9809e6 Andy McCall ↶ 191
        if !valid_reference_name(name) {
192
            return fail(format!("{name:?} is not a valid bookmark name"));
193
        }
194
        let (op_ref, op) = self.head_op()?;
195
        if op.view.bookmarks.contains_key(name) {
196
            return fail(format!("bookmark {name} already exists"));
197
        }
198
        let heads = match heads {
57eaac91 Andy McCall ↶ 199
            Some(h) => self.load_state_history(h)?.maximal(h),
7b9809e6 Andy McCall ↶ 200
            None => {
201
                let (_, wc) = self.working_copy()?;
57eaac91 Andy McCall ↶ 202
                self.load_state_history(&wc.deps)?.maximal(&wc.deps)
7b9809e6 Andy McCall ↶ 203
            }
204
        };
205
        let mut view = op.view.clone();
75167672 Andy McCall 206
        view.bookmarks.insert(
207
            name.to_string(),
208
            self.store.put_state(&StateObject::new(heads.clone()))?,
209
        );
7b9809e6 Andy McCall ↶ 210
        self.record_op(&[op_ref], &format!("oxy bookmark create {name}"), view)?;
211
        Ok(heads)
4fe7d604 Andy McCall ↶ 212
    }
213
214
    /// What a state at `at` lacks of the state at `from` (spec §10.1). A
215
    /// state is its heads and what they depend on, not everything recorded
216
    /// before them, so a bookmark created at one change of a state with
217
    /// several heads holds much less than someone used to Git expects.
218
    pub fn state_gap(&self, from: &[ChangeHash], at: &[ChangeHash]) -> Result<StateGap> {
219
        let mut all = from.to_vec();
220
        all.extend_from_slice(at);
221
        let st = self.load_state_history(&all)?;
222
        let with_closure = |hs: &[ChangeHash]| {
223
            let mut s: HashSet<ChangeHash> = hs.iter().copied().collect();
224
            for h in hs {
225
                s.extend(st.closure(h).iter().copied());
226
            }
227
            s
228
        };
229
        let from_set = with_closure(from);
230
        let at_set = with_closure(at);
231
        let key = |h: &ChangeHash| st.get(h).map(|m| (m.hlc, *h));
232
        let last = at.iter().filter_map(key).max();
233
        let missing: Vec<ChangeHash> = from_set
234
            .iter()
235
            .filter(|h| !at_set.contains(h))
236
            .copied()
237
            .collect();
238
        let mut earlier: Vec<ChangeHash> = missing
239
            .iter()
240
            .filter(|h| matches!((key(h), last), (Some(k), Some(l)) if k < l))
241
            .copied()
242
            .collect();
243
        earlier.sort_by_key(key);
244
        let mut candidates = at.to_vec();
245
        candidates.extend_from_slice(&earlier);
246
        Ok(StateGap {
247
            lacks: missing.len(),
248
            upto: st.maximal(&candidates),
249
            earlier,
250
        })
7b9809e6 Andy McCall ↶ 251
    }
252
253
    /// Point an existing bookmark at `heads`.
254
    pub fn bookmark_set(&self, name: &str, heads: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
255
        let (op_ref, op) = self.head_op()?;
256
        if !op.view.bookmarks.contains_key(name) {
75167672 Andy McCall 257
            return fail(format!(
258
                "no bookmark {name}; create it with `oxy bookmark create`"
259
            ));
7b9809e6 Andy McCall ↶ 260
        }
57eaac91 Andy McCall ↶ 261
        let heads = self.load_state_history(heads)?.maximal(heads);
7b9809e6 Andy McCall ↶ 262
        let mut view = op.view.clone();
75167672 Andy McCall 263
        view.bookmarks.insert(
264
            name.to_string(),
265
            self.store.put_state(&StateObject::new(heads.clone()))?,
266
        );
7b9809e6 Andy McCall ↶ 267
        self.record_op(&[op_ref], &format!("oxy bookmark set {name}"), view)?;
268
        if name == self.config.bookmark {
269
            self.drop_manifest();
270
        }
271
        Ok(heads)
272
    }
273
274
    /// Remove a bookmark; its changes stay until unreachable (spec §21).
275
    pub fn bookmark_delete(&self, name: &str) -> Result<()> {
276
        if name == self.config.bookmark {
75167672 Andy McCall 277
            return fail(format!(
278
                "{name} is this workspace's current bookmark; switch to another first"
279
            ));
7b9809e6 Andy McCall ↶ 280
        }
281
        for w in self.workspaces()? {
282
            if !w.current && self.workspace_bookmark_of(&w.id).as_deref() == Some(name) {
75167672 Andy McCall 283
                return fail(format!(
284
                    "{name} is the current bookmark of workspace {} at {}",
285
                    w.id.short(8),
286
                    w.path.display()
287
                ));
7b9809e6 Andy McCall ↶ 288
            }
289
        }
290
        let (op_ref, op) = self.head_op()?;
291
        let mut view = op.view.clone();
292
        if view.bookmarks.remove(name).is_none() {
293
            return fail(format!("no bookmark {name}"));
294
        }
295
        self.record_op(&[op_ref], &format!("oxy bookmark delete {name}"), view)?;
296
        Ok(())
297
    }
298
299
    fn workspace_bookmark_of(&self, id: &WorkspaceId) -> Option<String> {
75167672 Andy McCall 300
        fs::read_to_string(
301
            self.oxy
302
                .join("workspaces")
303
                .join(id.to_hex())
304
                .join("bookmark"),
305
        )
306
        .ok()
307
        .map(|s| s.trim().to_string())
308
        .filter(|s| !s.is_empty())
7b9809e6 Andy McCall ↶ 309
    }
310
311
    /// Make `name` this workspace's current bookmark and check it out.
312
    pub fn bookmark_switch(&mut self, name: &str, discard: bool) -> Result<CheckoutReport> {
6caacfff Andy McCall ↶ 313
        let _wc = self.lock_working_copy()?;
14edbaaf Andy McCall ↶ 314
        let (name, _) = self.local_bookmark_for(name)?;
315
        let name = name.as_str();
7b9809e6 Andy McCall ↶ 316
        let view = self.view()?;
75167672 Andy McCall 317
        let Some(st) = view.bookmarks.get(name) else {
318
            return fail(format!("no bookmark {name}"));
319
        };
7b9809e6 Andy McCall ↶ 320
        let heads = self.store.get_state(st)?.heads;
321
        let report = self.checkout(&heads, discard, &format!("oxy bookmark switch {name}"))?;
322
        fs::create_dir_all(self.workspace_bookmark_path().parent().unwrap())?;
323
        write_atomic(&self.workspace_bookmark_path(), name.as_bytes())?;
324
        self.config.bookmark = name.to_string();
325
        Ok(report)
326
    }
327
61e3bce7 Andy McCall ↶ 328
    /// Switch to `name` taking uncommitted work along (spec §10.1), as `git
1c2bd9ab Andy McCall ↶ 329
    /// switch` does: see [`Repo::checkout_carry`]. Returns the checkout
330
    /// report and the carried paths.
61e3bce7 Andy McCall ↶ 331
    pub fn bookmark_switch_carry(&mut self, name: &str) -> Result<(CheckoutReport, Vec<String>)> {
6caacfff Andy McCall ↶ 332
        let _wc = self.lock_working_copy()?;
14edbaaf Andy McCall ↶ 333
        let (name, _) = self.local_bookmark_for(name)?;
334
        let name = name.as_str();
61e3bce7 Andy McCall ↶ 335
        let view = self.view()?;
336
        let Some(st) = view.bookmarks.get(name) else {
337
            return fail(format!("no bookmark {name}"));
338
        };
339
        let heads = self.store.get_state(st)?.heads;
1c2bd9ab Andy McCall ↶ 340
        let out =
341
            self.checkout_carry(&heads, name, &format!("oxy bookmark switch {name} --carry"))?;
342
        fs::create_dir_all(self.workspace_bookmark_path().parent().unwrap())?;
343
        write_atomic(&self.workspace_bookmark_path(), name.as_bytes())?;
344
        self.config.bookmark = name.to_string();
345
        self.snapshot(&format!("oxy bookmark switch {name} --carry"))?;
346
        Ok(out)
347
    }
348
349
    /// Check out `heads` taking uncommitted work along: every path the
350
    /// working copy changes must be the same in the target state as in the
351
    /// working copy's base, so carrying the files over applies exactly the
352
    /// same edits there. Otherwise nothing happens and the error names the
353
    /// paths that differ (`target` names the state in it). Returns the
354
    /// checkout report and the carried paths.
355
    pub fn checkout_carry(
356
        &self,
357
        heads: &[ChangeHash],
358
        target: &str,
359
        command: &str,
360
    ) -> Result<(CheckoutReport, Vec<String>)> {
6caacfff Andy McCall ↶ 361
        let _wc = self.lock_working_copy()?;
1c2bd9ab Andy McCall ↶ 362
        let snap = self.snapshot(command)?;
61e3bce7 Andy McCall ↶ 363
        // Every path the work touches: changed ones, and base files it
364
        // removed or renamed away (a rename reports only its new path).
365
        let mut touched: BTreeSet<String> = snap.statuses.iter().map(|s| s.path.clone()).collect();
366
        for (p, f) in &snap.base_snapshot.files {
367
            if f.kind != NodeKind::Dir
368
                && f.kind != NodeKind::Subrepo
369
                && fs::symlink_metadata(self.root.join(p)).is_err()
370
            {
371
                touched.insert(p.clone());
372
            }
373
        }
1c2bd9ab Andy McCall ↶ 374
        let target_state = self.load_state(heads)?.materialize();
61e3bce7 Andy McCall ↶ 375
        let same = |p: &String| {
376
            let a = snap.base_snapshot.files.get(p);
1c2bd9ab Andy McCall ↶ 377
            let b = target_state.files.get(p);
61e3bce7 Andy McCall ↶ 378
            match (a, b) {
379
                (None, None) => true,
380
                (Some(a), Some(b)) => {
381
                    a.kind == b.kind && a.mode == b.mode && a.content == b.content
382
                }
383
                _ => false,
384
            }
385
        };
386
        let differ: Vec<&String> = touched.iter().filter(|p| !same(p)).collect();
387
        if !differ.is_empty() {
388
            let list: Vec<&str> = differ.iter().take(10).map(|p| p.as_str()).collect();
389
            return fail(format!(
1c2bd9ab Andy McCall ↶ 390
                "{target} has other versions of {} you changed ({}{}); commit first, or drop the changes with --discard",
61e3bce7 Andy McCall ↶ 391
                if differ.len() == 1 { "a file" } else { "files" },
392
                list.join(", "),
393
                if differ.len() > 10 { ", …" } else { "" }
394
            ));
395
        }
1a3b6203 Andy McCall ↶ 396
        // A subrepo whose pin is carried work keeps its own state: checking
397
        // it out at the target's pin would roll it back (oxygen#38).
398
        let keep: BTreeSet<String> = touched
399
            .iter()
400
            .filter(|p| {
401
                snap.base_snapshot
402
                    .files
403
                    .get(*p)
404
                    .or_else(|| target_state.files.get(*p))
405
                    .is_some_and(|f| f.kind == NodeKind::Subrepo)
406
            })
407
            .cloned()
408
            .collect();
61e3bce7 Andy McCall ↶ 409
        // The work as it is on disk: content and mode, or absent.
410
        let mut carried: Vec<(String, Carried)> = Vec::new();
411
        for p in &touched {
1a3b6203 Andy McCall ↶ 412
            if keep.contains(p) {
413
                carried.push((p.clone(), Carried::Subrepo));
414
                continue;
415
            }
61e3bce7 Andy McCall ↶ 416
            let full = self.root.join(p);
417
            let item = match fs::symlink_metadata(&full) {
418
                Ok(m) if m.file_type().is_symlink() => {
419
                    Carried::Symlink(fs::read_link(&full)?.to_string_lossy().as_bytes().to_vec())
420
                }
421
                Ok(m) if m.is_file() => Carried::File(is_executable(&m), fs::read(&full)?),
422
                Ok(_) => continue,
423
                Err(_) => Carried::Absent,
424
            };
425
            carried.push((p.clone(), item));
426
        }
1a3b6203 Andy McCall ↶ 427
        let report = self.checkout_keeping(heads, true, command, &keep)?;
61e3bce7 Andy McCall ↶ 428
        for (p, item) in &carried {
429
            match item {
430
                Carried::Symlink(target) => self.write_symlink(p, target)?,
431
                Carried::File(exec, bytes) => self.write_file(p, bytes, *exec)?,
432
                Carried::Absent => {
433
                    let _ = fs::remove_file(self.root.join(p));
434
                }
1a3b6203 Andy McCall ↶ 435
                Carried::Subrepo => {}
61e3bce7 Andy McCall ↶ 436
            }
437
        }
438
        self.drop_manifest();
1c2bd9ab Andy McCall ↶ 439
        self.snapshot(command)?;
61e3bce7 Andy McCall ↶ 440
        Ok((report, carried.into_iter().map(|(p, _)| p).collect()))
441
    }
442
7b9809e6 Andy McCall ↶ 443
    /// Record `name` as the current bookmark of a new workspace.
444
    pub(crate) fn set_workspace_bookmark(&self, id: &WorkspaceId, name: &str) -> Result<()> {
445
        let dir = self.oxy.join("workspaces").join(id.to_hex());
446
        fs::create_dir_all(&dir)?;
447
        write_atomic(&dir.join("bookmark"), name.as_bytes())
448
    }
449
450
    /// Union the heads of bookmark `name` into the current bookmark (spec
1c2bd9ab Andy McCall ↶ 451
    /// §10.1). A working copy without changes follows.
7b9809e6 Andy McCall ↶ 452
    pub fn merge(&self, name: &str) -> Result<MergeReport> {
6caacfff Andy McCall ↶ 453
        let _wc = self.lock_working_copy()?;
e266b7d4 Andy McCall ↶ 454
        let (_, op) = self.head_op()?;
455
        if name == self.config.bookmark {
7b9809e6 Andy McCall ↶ 456
            return fail(format!("{name} is the current bookmark"));
457
        }
75167672 Andy McCall 458
        let Some(st) = op.view.bookmarks.get(name) else {
459
            return fail(format!("no bookmark {name}"));
460
        };
7b9809e6 Andy McCall ↶ 461
        let other = self.store.get_state(st)?.heads;
e266b7d4 Andy McCall ↶ 462
        self.merge_heads(other, &format!("oxy merge {name}"))
463
    }
464
465
    /// Bring CHANGE up to date with `onto` where a rebase would be
466
    /// conflicted (oxygen#42): the state of `onto` is merged into the
467
    /// current bookmark, which must hold the change, and a working copy
468
    /// without changes follows, so the conflict is resolved there and
469
    /// committed on top, as after `oxy merge`.
470
    pub fn rebase_into_working_copy(
471
        &self,
472
        c: &ChangeHash,
473
        onto: &[ChangeHash],
474
    ) -> Result<MergeReport> {
475
        let _wc = self.lock_working_copy()?;
476
        let (_, op) = self.head_op()?;
477
        let current = self.config.bookmark.clone();
478
        let local = self.heads_of(&op.view, &current)?;
479
        if !self.load_state_indexed(&local)?.contains(c) {
480
            return fail(format!(
481
                "#{} is not on the current bookmark {current}; switch to the bookmark \
482
                 holding it first",
483
                c.short(12)
484
            ));
485
        }
486
        let named: Vec<String> = onto.iter().map(|h| h.short(12)).collect();
487
        let label = format!(
488
            "oxy rebase {} --onto {} --into-working-copy",
489
            c.short(12),
490
            named.join(" ")
491
        );
492
        self.merge_heads(onto.to_vec(), &label)
493
    }
494
495
    /// Union `other` into the current bookmark's heads, recorded as
496
    /// `label`; a working copy without changes follows.
497
    fn merge_heads(&self, other: Vec<ChangeHash>, label: &str) -> Result<MergeReport> {
498
        let (op_ref, op) = self.head_op()?;
499
        let current = self.config.bookmark.clone();
7b9809e6 Andy McCall ↶ 500
        let local = self.heads_of(&op.view, &current)?;
501
        let mut all = local.clone();
502
        all.extend(other);
57eaac91 Andy McCall ↶ 503
        let mut heads = self.load_state_history(&all)?.maximal(&all);
7b9809e6 Andy McCall ↶ 504
        heads.sort();
505
        let mut sorted_local = local.clone();
506
        sorted_local.sort();
507
        if heads == sorted_local {
75167672 Andy McCall 508
            return Ok(MergeReport {
509
                heads,
510
                changed: false,
511
                checked_out: false,
512
            });
7b9809e6 Andy McCall ↶ 513
        }
514
        let mut view = op.view.clone();
75167672 Andy McCall 515
        view.bookmarks.insert(
516
            current,
517
            self.store.put_state(&StateObject::new(heads.clone()))?,
518
        );
e266b7d4 Andy McCall ↶ 519
        self.record_op(&[op_ref], label, view)?;
520
        let snap = self.snapshot(label)?;
7b9809e6 Andy McCall ↶ 521
        let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
522
        let mut base = snap.base.maximal(&snap.wc.deps);
523
        base.sort();
524
        let mut checked_out = false;
1c2bd9ab Andy McCall ↶ 525
        // A working copy without changes follows the merge, whichever state
526
        // it sat on: nothing of the user's can be lost.
527
        if clean {
e266b7d4 Andy McCall ↶ 528
            self.checkout(&heads, false, &format!("{label} (checkout)"))?;
7b9809e6 Andy McCall ↶ 529
            checked_out = true;
530
        }
75167672 Andy McCall 531
        Ok(MergeReport {
532
            heads,
533
            changed: true,
534
            checked_out,
535
        })
7b9809e6 Andy McCall ↶ 536
    }
61e3bce7 Andy McCall ↶ 537
}
538
539
/// A path's state on disk, taken along by `bookmark switch --carry`.
540
enum Carried {
1a3b6203 Andy McCall ↶ 541
    /// A subrepo whose pin is carried: left as it is.
542
    Subrepo,
61e3bce7 Andy McCall ↶ 543
    Symlink(Vec<u8>),
544
    /// Executable bit and content.
545
    File(bool, Vec<u8>),
546
    Absent,
7b9809e6 Andy McCall ↶ 547
}