OXYGENHUB

Coverage / crates/oxy-core/src/repo/bookmarks.rs 96.2%

400 of 416 lines run, 67 of 90 functions · run #251 on main · run missed

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 the default remote'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 `..`, `//`, `@{`.
2825×
pub fn valid_reference_name(name: &str) -> bool {
2925×
    !name.is_empty()
3025×
        && name.len() <= 255
3125×
        && !name.starts_with('-')
32160×
        && !name.chars().any(|c| c.is_control())
3324×
        && !name.contains("..")
3423×
        && !name.contains("//")
3523×
        && !name.contains("@{")
3623×
        && name != "policy"
3725×
}
38
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.
4258×
pub fn remote_bookmark<'a>(view: &'a View, spec: &str) -> Option<(&'a str, &'a str, ObjectRef)> {
4358×
    view.remotes
4458×
        .iter()
4558×
        .filter_map(|(remote, bookmarks)| {
4611×
            let rest = spec.strip_prefix(remote.as_str())?.strip_prefix('/')?;
473×
            let (name, state) = bookmarks.get_key_value(rest)?;
483×
            Some((remote.as_str(), name.as_str(), *state))
4911×
        })
5058×
        .max_by_key(|(remote, _, _)| remote.len())
5158×
}
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
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
75
impl Repo {
7698×
    pub(crate) fn workspace_bookmark_path(&self) -> PathBuf {
7798×
        self.oxy
7898×
            .join("workspaces")
7998×
            .join(self.workspace.to_hex())
8098×
            .join("bookmark")
8198×
    }
82
83
    /// The remote a command uses when none is named: `origin`, else the
84
    /// only remote there is (finding 29 of the metalgear feedback: a
85
    /// checkout whose one remote is `earth`).
8635×
    pub fn default_remote(&self) -> String {
8735×
        match self.config.remotes.keys().collect::<Vec<_>>().as_slice() {
8827×
            [only] if *only != "origin" => (*only).clone(),
8933×
            _ => "origin".into(),
90
        }
9135×
    }
92
93
    /// How bookmark `name` stands against `remote`'s copy of it: the
94
    /// changes only here and only there. `None` when either side has no
95
    /// such bookmark.
966×
    pub fn tracking(&self, remote: &str, name: &str) -> Result<Option<(usize, usize)>> {
976×
        let view = self.view()?;
985×
        let (Some(st), Some(rs)) = (
996×
            view.bookmarks.get(name),
1006×
            view.remotes.get(remote).and_then(|b| b.get(name)),
101
        ) else {
1021×
            return Ok(None);
103
        };
1045×
        let heads = self.store.get_state(st)?.heads;
1055×
        let remote_heads = self.store.get_state(rs)?.heads;
106
        Ok(Some((
1075×
            self.negotiate(&remote_heads, &heads)?.len(),
1085×
            self.negotiate(&heads, &remote_heads)?.len(),
109
        )))
1106×
    }
111
112
    /// Every bookmark with its heads and its difference from the default
113
    /// remote's copy.
11412×
    pub fn bookmarks(&self) -> Result<Vec<BookmarkInfo>> {
11512×
        let view = self.view()?;
11612×
        let remote = self.default_remote();
11712×
        let mut out = Vec::new();
11825×
        for (name, st) in &view.bookmarks {
11925×
            let heads = self.store.get_state(st)?.heads;
12025×
            let (ahead, behind) = match view.remotes.get(&remote).and_then(|b| b.get(name)) {
12110×
                Some(rs) => {
12210×
                    let remote_heads = self.store.get_state(rs)?.heads;
123
                    (
12410×
                        self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok(),
12510×
                        self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok(),
126
                    )
127
                }
12815×
                None => (None, None),
129
            };
13025×
            out.push(BookmarkInfo {
13125×
                name: name.clone(),
13225×
                heads,
13325×
                current: *name == self.config.bookmark,
13425×
                ahead,
13525×
                behind,
13625×
            });
137
        }
13812×
        Ok(out)
13912×
    }
140
141
    /// Bookmarks of remotes with no local bookmark of the same name, by
142
    /// remote then name.
14311×
    pub fn remote_only_bookmarks(&self) -> Result<Vec<RemoteOnlyBookmark>> {
14411×
        let view = self.view()?;
14511×
        let mut out = Vec::new();
14611×
        for (remote, bookmarks) in &view.remotes {
14710×
            for (name, st) in bookmarks {
14810×
                if view.bookmarks.contains_key(name) {
1499×
                    continue;
1501×
                }
1511×
                out.push(RemoteOnlyBookmark {
1521×
                    remote: remote.clone(),
1531×
                    name: name.clone(),
1541×
                    heads: self.store.get_state(st)?.heads,
155
                });
156
            }
157
        }
15811×
        Ok(out)
15911×
    }
160
161
    /// The local bookmark to switch to for `spec`, creating it from a
162
    /// remote's when there is none (as `git switch` does): `NAME` takes
163
    /// `origin/NAME` (or the one remote that has it), and `REMOTE/NAME`
164
    /// takes that one. Returns the local name and, when it was just
165
    /// created, the remote it came from. A local bookmark tracks the
166
    /// remote's of the same name: `oxy pull` updates it.
16752×
    pub fn local_bookmark_for(&self, spec: &str) -> Result<(String, Option<String>)> {
16852×
        let (op_ref, op) = self.head_op()?;
16952×
        if op.view.bookmarks.contains_key(spec) {
17049×
            return Ok((spec.to_string(), None));
1713×
        }
1723×
        let (remote, name, state) = match remote_bookmark(&op.view, spec) {
1731×
            Some((remote, name, state)) => (remote.to_string(), name.to_string(), state),
174
            None => {
1752×
                let mut having: Vec<&String> = op
1762×
                    .view
1772×
                    .remotes
1782×
                    .iter()
1792×
                    .filter(|(_, b)| b.contains_key(spec))
1802×
                    .map(|(r, _)| r)
1812×
                    .collect();
1822×
                if let Some(i) = having.iter().position(|r| *r == "origin") {
1831×
                    having = vec![having[i]];
1841×
                }
1852×
                match having.as_slice() {
1862×
                    [] => return fail(format!("no bookmark {spec}, here or on a remote")),
1871×
                    [r] => {
1881×
                        let state = op.view.remotes[*r][spec];
1891×
                        (r.to_string(), spec.to_string(), state)
190
                    }
1910×
                    several => {
1920×
                        let names: Vec<String> =
1930×
                            several.iter().map(|r| format!("{r}/{spec}")).collect();
1940×
                        return fail(format!(
195
                            "several remotes have {spec}: name one, as {}",
1960×
                            names.join(" or ")
197
                        ));
198
                    }
199
                }
200
            }
201
        };
2022×
        if op.view.bookmarks.contains_key(&name) {
203
            // REMOTE/NAME where NAME is already local: switch to the local one.
2040×
            return Ok((name, None));
2052×
        }
2062×
        let mut view = op.view.clone();
2072×
        view.bookmarks.insert(name.clone(), state);
2082×
        self.record_op(
2092×
            &[op_ref],
2102×
            &format!("oxy bookmark track {remote}/{name}"),
2112×
            view,
2120×
        )?;
2132×
        Ok((name, Some(remote)))
21452×
    }
215
216
    /// Create `name` at `heads` (default: the working copy's base heads).
21725×
    pub fn bookmark_create(
21825×
        &self,
21925×
        name: &str,
22025×
        heads: Option<&[ChangeHash]>,
22125×
    ) -> Result<Vec<ChangeHash>> {
22225×
        if !valid_reference_name(name) {
2232×
            return fail(format!("{name:?} is not a valid bookmark name"));
22423×
        }
22523×
        let (op_ref, op) = self.head_op()?;
22623×
        if op.view.bookmarks.contains_key(name) {
2271×
            return fail(format!("bookmark {name} already exists"));
22822×
        }
22922×
        let heads = match heads {
2303×
            Some(h) => self.load_state_history(h)?.maximal(h),
231
            None => {
23219×
                let (_, wc) = self.working_copy()?;
23319×
                self.load_state_history(&wc.deps)?.maximal(&wc.deps)
234
            }
235
        };
23622×
        let mut view = op.view.clone();
23722×
        view.bookmarks.insert(
23822×
            name.to_string(),
23922×
            self.store.put_state(&StateObject::new(heads.clone()))?,
240
        );
24122×
        self.record_op(&[op_ref], &format!("oxy bookmark create {name}"), view)?;
24222×
        Ok(heads)
24325×
    }
244
245
    /// What a state at `at` lacks of the state at `from` (spec §10.1). A
246
    /// state is its heads and what they depend on, not everything recorded
247
    /// before them, so a bookmark created at one change of a state with
248
    /// several heads holds much less than someone used to Git expects.
2497×
    pub fn state_gap(&self, from: &[ChangeHash], at: &[ChangeHash]) -> Result<StateGap> {
2507×
        let mut all = from.to_vec();
2517×
        all.extend_from_slice(at);
2527×
        let st = self.load_state_history(&all)?;
25314×
        let with_closure = |hs: &[ChangeHash]| {
25414×
            let mut s: HashSet<ChangeHash> = hs.iter().copied().collect();
25524×
            for h in hs {
25624×
                s.extend(st.closure(h).iter().copied());
25724×
            }
25814×
            s
25914×
        };
2607×
        let from_set = with_closure(from);
2617×
        let at_set = with_closure(at);
26218×
        let key = |h: &ChangeHash| st.get(h).map(|m| (m.hlc, *h));
2637×
        let last = at.iter().filter_map(key).max();
2647×
        let missing: Vec<ChangeHash> = from_set
2657×
            .iter()
26619×
            .filter(|h| !at_set.contains(h))
2677×
            .copied()
2687×
            .collect();
2697×
        let mut earlier: Vec<ChangeHash> = missing
2707×
            .iter()
27111×
            .filter(|h| matches!((key(h), last), (Some(k), Some(l)) if k < l))
2727×
            .copied()
2737×
            .collect();
2747×
        earlier.sort_by_key(key);
2757×
        let mut candidates = at.to_vec();
2767×
        candidates.extend_from_slice(&earlier);
2777×
        Ok(StateGap {
2787×
            lacks: missing.len(),
2797×
            upto: st.maximal(&candidates),
2807×
            earlier,
2817×
        })
2827×
    }
283
284
    /// Point an existing bookmark at `heads`.
2855×
    pub fn bookmark_set(&self, name: &str, heads: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
2865×
        let (op_ref, op) = self.head_op()?;
2875×
        if !op.view.bookmarks.contains_key(name) {
2881×
            return fail(format!(
289
                "no bookmark {name}; create it with `oxy bookmark create`"
290
            ));
2914×
        }
2924×
        let heads = self.load_state_history(heads)?.maximal(heads);
2934×
        let mut view = op.view.clone();
2944×
        view.bookmarks.insert(
2954×
            name.to_string(),
2964×
            self.store.put_state(&StateObject::new(heads.clone()))?,
297
        );
2984×
        self.record_op(&[op_ref], &format!("oxy bookmark set {name}"), view)?;
2994×
        if name == self.config.bookmark {
3002×
            self.drop_manifest();
3012×
        }
3024×
        Ok(heads)
3035×
    }
304
305
    /// Remove a bookmark; its changes stay until unreachable (spec §21).
3063×
    pub fn bookmark_delete(&self, name: &str) -> Result<()> {
3073×
        if name == self.config.bookmark {
3081×
            return fail(format!(
309
                "{name} is this workspace's current bookmark; switch to another first"
310
            ));
3112×
        }
3124×
        for w in self.workspaces()? {
3134×
            if !w.current && self.workspace_bookmark_of(&w.id).as_deref() == Some(name) {
3141×
                return fail(format!(
315
                    "{name} is the current bookmark of workspace {} at {}",
3161×
                    w.id.short(8),
3171×
                    w.path.display()
318
                ));
3193×
            }
320
        }
3211×
        let (op_ref, op) = self.head_op()?;
3221×
        let mut view = op.view.clone();
3231×
        if view.bookmarks.remove(name).is_none() {
3240×
            return fail(format!("no bookmark {name}"));
3251×
        }
3261×
        self.record_op(&[op_ref], &format!("oxy bookmark delete {name}"), view)?;
3271×
        Ok(())
3283×
    }
329
3302×
    fn workspace_bookmark_of(&self, id: &WorkspaceId) -> Option<String> {
3312×
        fs::read_to_string(
3322×
            self.oxy
3332×
                .join("workspaces")
3342×
                .join(id.to_hex())
3352×
                .join("bookmark"),
336
        )
3372×
        .ok()
3382×
        .map(|s| s.trim().to_string())
3392×
        .filter(|s| !s.is_empty())
3402×
    }
341
342
    /// Make `name` this workspace's current bookmark and check it out.
34347×
    pub fn bookmark_switch(&mut self, name: &str, discard: bool) -> Result<CheckoutReport> {
34447×
        let _wc = self.lock_working_copy()?;
34547×
        let (name, _) = self.local_bookmark_for(name)?;
34646×
        let name = name.as_str();
34746×
        let view = self.view()?;
34846×
        let Some(st) = view.bookmarks.get(name) else {
3490×
            return fail(format!("no bookmark {name}"));
350
        };
35146×
        let heads = self.store.get_state(st)?.heads;
35246×
        let report = self.checkout(&heads, discard, &format!("oxy bookmark switch {name}"))?;
35346×
        fs::create_dir_all(self.workspace_bookmark_path().parent().unwrap())?;
35446×
        write_atomic(&self.workspace_bookmark_path(), name.as_bytes())?;
35546×
        self.config.bookmark = name.to_string();
35646×
        Ok(report)
35747×
    }
358
359
    /// Switch to `name` taking uncommitted work along (spec §10.1), as `git
360
    /// switch` does: see [`Repo::checkout_carry`]. Returns the checkout
361
    /// report and the carried paths.
3623×
    pub fn bookmark_switch_carry(&mut self, name: &str) -> Result<(CheckoutReport, Vec<String>)> {
3633×
        let _wc = self.lock_working_copy()?;
3643×
        let (name, _) = self.local_bookmark_for(name)?;
3653×
        let name = name.as_str();
3663×
        let view = self.view()?;
3673×
        let Some(st) = view.bookmarks.get(name) else {
3680×
            return fail(format!("no bookmark {name}"));
369
        };
3703×
        let heads = self.store.get_state(st)?.heads;
3712×
        let out =
3723×
            self.checkout_carry(&heads, name, &format!("oxy bookmark switch {name} --carry"))?;
3732×
        fs::create_dir_all(self.workspace_bookmark_path().parent().unwrap())?;
3742×
        write_atomic(&self.workspace_bookmark_path(), name.as_bytes())?;
3752×
        self.config.bookmark = name.to_string();
3762×
        self.snapshot(&format!("oxy bookmark switch {name} --carry"))?;
3772×
        Ok(out)
3783×
    }
379
380
    /// Check out `heads` taking uncommitted work along: every path the
381
    /// working copy changes must be the same in the target state as in the
382
    /// working copy's base, so carrying the files over applies exactly the
383
    /// same edits there. Otherwise nothing happens and the error names the
384
    /// paths that differ (`target` names the state in it). Returns the
385
    /// checkout report and the carried paths.
3865×
    pub fn checkout_carry(
3875×
        &self,
3885×
        heads: &[ChangeHash],
3895×
        target: &str,
3905×
        command: &str,
3915×
    ) -> Result<(CheckoutReport, Vec<String>)> {
3925×
        let _wc = self.lock_working_copy()?;
3935×
        let snap = self.snapshot(command)?;
394
        // Every path the work touches: changed ones, and base files it
395
        // removed or renamed away (a rename reports only its new path).
3968×
        let mut touched: BTreeSet<String> = snap.statuses.iter().map(|s| s.path.clone()).collect();
39716×
        for (p, f) in &snap.base_snapshot.files {
39816×
            if f.kind != NodeKind::Dir
39915×
                && f.kind != NodeKind::Subrepo
40014×
                && fs::symlink_metadata(self.root.join(p)).is_err()
4012×
            {
4022×
                touched.insert(p.clone());
40314×
            }
404
        }
4055×
        let target_state = self.load_state(heads)?.materialize();
4068×
        let same = |p: &String| {
4078×
            let a = snap.base_snapshot.files.get(p);
4088×
            let b = target_state.files.get(p);
4098×
            match (a, b) {
4101×
                (None, None) => true,
4116×
                (Some(a), Some(b)) => {
4126×
                    a.kind == b.kind && a.mode == b.mode && a.content == b.content
413
                }
4141×
                _ => false,
415
            }
4168×
        };
4178×
        let differ: Vec<&String> = touched.iter().filter(|p| !same(p)).collect();
4185×
        if !differ.is_empty() {
4192×
            let list: Vec<&str> = differ.iter().take(10).map(|p| p.as_str()).collect();
4202×
            return fail(format!(
421
                "{target} has other versions of {} you changed ({}{}); commit first, or drop the changes with --discard",
4222×
                if differ.len() == 1 { "a file" } else { "files" },
4232×
                list.join(", "),
4242×
                if differ.len() > 10 { ", …" } else { "" }
425
            ));
4263×
        }
427
        // A subrepo whose pin is carried work keeps its own state: checking
428
        // it out at the target's pin would roll it back (oxygen#38).
4293×
        let keep: BTreeSet<String> = touched
4303×
            .iter()
4315×
            .filter(|p| {
4325×
                snap.base_snapshot
4335×
                    .files
4345×
                    .get(*p)
4355×
                    .or_else(|| target_state.files.get(*p))
4365×
                    .is_some_and(|f| f.kind == NodeKind::Subrepo)
4375×
            })
4383×
            .cloned()
4393×
            .collect();
440
        // The work as it is on disk: content and mode, or absent.
4413×
        let mut carried: Vec<(String, Carried)> = Vec::new();
4425×
        for p in &touched {
4435×
            if keep.contains(p) {
4441×
                carried.push((p.clone(), Carried::Subrepo));
4451×
                continue;
4464×
            }
4474×
            let full = self.root.join(p);
4484×
            let item = match fs::symlink_metadata(&full) {
4493×
                Ok(m) if m.file_type().is_symlink() => {
4500×
                    Carried::Symlink(fs::read_link(&full)?.to_string_lossy().as_bytes().to_vec())
451
                }
4523×
                Ok(m) if m.is_file() => Carried::File(is_executable(&m), fs::read(&full)?),
4530×
                Ok(_) => continue,
4541×
                Err(_) => Carried::Absent,
455
            };
4564×
            carried.push((p.clone(), item));
457
        }
4583×
        let report = self.checkout_keeping(heads, true, command, &keep)?;
4595×
        for (p, item) in &carried {
4605×
            match item {
4610×
                Carried::Symlink(target) => self.write_symlink(p, target)?,
4623×
                Carried::File(exec, bytes) => self.write_file(p, bytes, *exec)?,
4631×
                Carried::Absent => {
4641×
                    let _ = fs::remove_file(self.root.join(p));
4651×
                }
4661×
                Carried::Subrepo => {}
467
            }
468
        }
4693×
        self.drop_manifest();
4703×
        self.snapshot(command)?;
4713×
        Ok((report, carried.into_iter().map(|(p, _)| p).collect()))
4725×
    }
473
474
    /// Record `name` as the current bookmark of a new workspace.
4754×
    pub(crate) fn set_workspace_bookmark(&self, id: &WorkspaceId, name: &str) -> Result<()> {
4764×
        let dir = self.oxy.join("workspaces").join(id.to_hex());
4774×
        fs::create_dir_all(&dir)?;
4784×
        write_atomic(&dir.join("bookmark"), name.as_bytes())
4794×
    }
480
481
    /// Union the heads of bookmark `name` into the current bookmark (spec
482
    /// §10.1). A working copy without changes follows.
4835×
    pub fn merge(&self, name: &str) -> Result<MergeReport> {
4845×
        let _wc = self.lock_working_copy()?;
4855×
        let (_, op) = self.head_op()?;
4865×
        if name == self.config.bookmark {
4871×
            return fail(format!("{name} is the current bookmark"));
4884×
        }
4894×
        let Some(st) = op.view.bookmarks.get(name) else {
4900×
            return fail(format!("no bookmark {name}"));
491
        };
4924×
        let other = self.store.get_state(st)?.heads;
4934×
        self.merge_heads(other, &format!("oxy merge {name}"))
4945×
    }
495
496
    /// Bring CHANGE up to date with `onto` where a rebase would be
497
    /// conflicted (oxygen#42): the state of `onto` is merged into the
498
    /// current bookmark, which must hold the change, and a working copy
499
    /// without changes follows, so the conflict is resolved there and
500
    /// committed on top, as after `oxy merge`.
5012×
    pub fn rebase_into_working_copy(
5022×
        &self,
5032×
        c: &ChangeHash,
5042×
        onto: &[ChangeHash],
5052×
    ) -> Result<MergeReport> {
5062×
        let _wc = self.lock_working_copy()?;
5072×
        let (_, op) = self.head_op()?;
5082×
        let current = self.config.bookmark.clone();
5092×
        let local = self.heads_of(&op.view, &current)?;
5102×
        if !self.load_state_indexed(&local)?.contains(c) {
5111×
            return fail(format!(
512
                "#{} is not on the current bookmark {current}; switch to the bookmark \
513
                 holding it first",
5141×
                c.short(12)
515
            ));
5161×
        }
5171×
        let named: Vec<String> = onto.iter().map(|h| h.short(12)).collect();
5181×
        let label = format!(
519
            "oxy rebase {} --onto {} --into-working-copy",
5201×
            c.short(12),
5211×
            named.join(" ")
522
        );
5231×
        self.merge_heads(onto.to_vec(), &label)
5242×
    }
525
526
    /// Union `other` into the current bookmark's heads, recorded as
527
    /// `label`; a working copy without changes follows.
5285×
    fn merge_heads(&self, other: Vec<ChangeHash>, label: &str) -> Result<MergeReport> {
5295×
        let (op_ref, op) = self.head_op()?;
5305×
        let current = self.config.bookmark.clone();
5315×
        let local = self.heads_of(&op.view, &current)?;
5325×
        let mut all = local.clone();
5335×
        all.extend(other);
5345×
        let mut heads = self.load_state_history(&all)?.maximal(&all);
5355×
        heads.sort();
5365×
        let mut sorted_local = local.clone();
5375×
        sorted_local.sort();
5385×
        if heads == sorted_local {
5391×
            return Ok(MergeReport {
5401×
                heads,
5411×
                changed: false,
5421×
                checked_out: false,
5431×
            });
5444×
        }
5454×
        let mut view = op.view.clone();
5464×
        view.bookmarks.insert(
5474×
            current,
5484×
            self.store.put_state(&StateObject::new(heads.clone()))?,
549
        );
5504×
        self.record_op(&[op_ref], label, view)?;
5514×
        let snap = self.snapshot(label)?;
5524×
        let clean = snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty();
5534×
        let mut base = snap.base.maximal(&snap.wc.deps);
5544×
        base.sort();
5554×
        let mut checked_out = false;
556
        // A working copy without changes follows the merge, whichever state
557
        // it sat on: nothing of the user's can be lost.
5584×
        if clean {
5594×
            self.checkout(&heads, false, &format!("{label} (checkout)"))?;
5604×
            checked_out = true;
5610×
        }
5624×
        Ok(MergeReport {
5634×
            heads,
5644×
            changed: true,
5654×
            checked_out,
5664×
        })
5675×
    }
568
}
569
570
/// A path's state on disk, taken along by `bookmark switch --carry`.
571
enum Carried {
572
    /// A subrepo whose pin is carried: left as it is.
573
    Subrepo,
574
    Symlink(Vec<u8>),
575
    /// Executable bit and content.
576
    File(bool, Vec<u8>),
577
    Absent,
578
}