Skip to main content

lattice_magit/
workdir.rs

1//! B3: repository discovery, in one place.
2//!
3//! Every magit mode needs "where is the repository", and every one of
4//! them spelled it out — eleven copies of
5//! `Repository::discover(…).ok().and_then(|r| r.workdir().map(…))`.
6//!
7//! **This is not tidying.** `gix::discover` takes a *directory*, and
8//! passing it a file path fails silently: `discover` returns `Err`, the
9//! `.ok()` swallows it, and the caller gets a default. MG.11 found
10//! three sites doing exactly that, one of them in `lattice-host`'s
11//! auto-head-diff subsystem — which meant gutter diff signs had never
12//! worked, for any file, since they landed. Two functions, one per
13//! question, make that mistake unrepresentable: a caller with a file
14//! path cannot reach the directory-taking one.
15
16use std::path::{Path, PathBuf};
17
18use lattice_vcs::Repository;
19
20/// The working directory of the repository containing the current
21/// directory.
22///
23/// `None` for "not in a repository" and for a bare one (which has no
24/// working tree, so there is nothing for a magit buffer to show). Most
25/// callers want `.unwrap_or_default()` — an empty path makes the
26/// subsequent git call fail and the buffer say so, which is the same
27/// outcome the hand-written copies produced.
28pub(crate) fn magit_workdir() -> Option<PathBuf> {
29    magit_workdir_from(Path::new("."))
30}
31
32/// [`magit_workdir`] discovering from `start` rather than the process's
33/// working directory.
34///
35/// PR.5: the no-arg form discovers from `"."`, which is the **process**
36/// cwd — and `:cd` sets `editor.current_dir` without ever calling
37/// `set_current_dir`, so after `:cd /other/repo` a fresh `C-x g` still
38/// opened the repository the editor was launched in. Callers that can
39/// reach the project resolver pass its answer here instead
40/// (`RepoScopes::discovery_start`).
41///
42/// `start` is where discovery *begins*, not the answer: magit needs a
43/// git worktree specifically and a project root need not be one, so
44/// `gix` still walks up and still returns `None` outside a repository.
45pub(crate) fn magit_workdir_from(start: &Path) -> Option<PathBuf> {
46    let repo = Repository::discover(start).ok()?;
47    let workdir = repo.workdir()?.to_path_buf();
48    // MR.3b: discovery from `.` reports the workdir RELATIVE to the
49    // current directory, so an editor started in `crates/lattice-ui-tui`
50    // gets `../..`. That was harmless while the path was only ever
51    // handed to `git -C`; it is not harmless now that the path is
52    // labelled and compared:
53    //
54    //   - `repo_label("../..")` has no final component, so the buffer
55    //     came out as `*magit:log:../..*` — a name for no repository in
56    //     particular, and a different one from every other directory.
57    //   - the record compares workdir paths to detect a basename
58    //     collision, and `../..` and `/Users/…/lattice` are different
59    //     paths for the same checkout.
60    //
61    // Canonicalising here rather than at each caller keeps the one
62    // relative source in one place. Falls back to the raw path if the
63    // filesystem refuses, which is what it did before.
64    Some(std::fs::canonicalize(&workdir).unwrap_or(workdir))
65}
66
67/// The `(workdir, repo-relative-path)` pair for a file on disk.
68///
69/// Takes the file's **parent** to `discover`, which is the whole point
70/// of this existing separately — see the module note. The relative
71/// path falls back to the input when it does not sit under the
72/// workdir, which is what a symlinked or otherwise unexpected path
73/// does; git will then reject it by name rather than silently acting
74/// on something else.
75pub(crate) fn workdir_for_file(path: &Path) -> Option<(PathBuf, PathBuf)> {
76    let repo = Repository::discover(path.parent()?).ok()?;
77    let workdir = repo.workdir()?.to_path_buf();
78    let rel = path.strip_prefix(&workdir).unwrap_or(path).to_path_buf();
79    Some((workdir, rel))
80}
81
82/// MR.1: which repository a magit trigger should act on.
83///
84/// Design: `docs/dev/architecture/magit-repo-scoping.md` §2. Three
85/// questions, in this order, and the order is the design:
86///
87/// 1. **`from_magit_buffer`** — the active buffer is itself a magit
88///    buffer, so use the repository it is already showing. Without this,
89///    `C-x g` inside repo B's log would jump back to the cwd repo: a
90///    magit chord pressed inside magit would silently change which
91///    repository you are working on.
92/// 2. **`active_file`** — the file in front of you decides. This is the
93///    point of the change: opening a file from another checkout and
94///    acting on *its* repository, without restarting the editor there.
95/// 3. **cwd** — what every magit surface did unconditionally before.
96///    Kept as the fallback so a fresh editor with nothing open still
97///    answers `C-x g`; that is what makes this a widening rather than a
98///    trade.
99///
100/// Takes its inputs rather than reaching for the active buffer itself:
101/// the callers (an action handler, an ex-command, a transient row) reach
102/// the active buffer by different routes, and a resolver that picked one
103/// would be usable from only some of them.
104pub(crate) fn repo_for_trigger(
105    from_magit_buffer: Option<PathBuf>,
106    active_file: Option<&Path>,
107    fallback_start: Option<&Path>,
108) -> Option<PathBuf> {
109    if let Some(workdir) = from_magit_buffer {
110        return Some(workdir);
111    }
112    if let Some(path) = active_file
113        && let Some((workdir, _rel)) = workdir_for_file(path)
114    {
115        return Some(workdir);
116    }
117    // PR.5: step 3 discovers from the project resolver's answer when one
118    // is reachable, so `:cd` is honoured. `None` keeps the pre-PR.5
119    // behaviour for harnesses with no resolver registered.
120    magit_workdir_from(fallback_start.unwrap_or(Path::new(".")))
121}
122
123/// MR.1: the human-readable repository label that goes in a magit
124/// buffer's name — the repository directory's basename.
125///
126/// **Not the source of truth for where git runs.** A basename cannot
127/// round-trip to a path, and two checkouts can share one
128/// (`~/work/api`, `~/oss/api`), so the workdir is carried beside the
129/// buffer rather than parsed back out of its name (design §3.1). This
130/// exists because `:ls` is read far more often than a buffer name is
131/// parsed, and `*magit:status:/Users/…/src/lattice*` is not a name.
132pub(crate) fn repo_label(workdir: &Path) -> String {
133    workdir
134        .file_name()
135        .map(|n| n.to_string_lossy().into_owned())
136        // A workdir with no final component is the filesystem root, or a
137        // path ending in `..`. Rare and not worth an unnamed buffer, so
138        // fall back to the whole path rather than to an empty label.
139        .unwrap_or_else(|| workdir.to_string_lossy().into_owned())
140}
141
142/// MR.1: the label qualified with its parent directory, for when two
143/// repositories share a basename.
144///
145/// Reached only on collision (design §3.1): the alternative is two
146/// repositories sharing one buffer, with the staging chords acting on
147/// whichever was recorded last — the worst outcome available here.
148pub(crate) fn qualified_repo_label(workdir: &Path) -> String {
149    let base = repo_label(workdir);
150    match workdir.parent().and_then(|p| p.file_name()) {
151        Some(parent) => format!("{}/{base}", parent.to_string_lossy()),
152        None => base,
153    }
154}
155
156/// MR.3: the shape every magit view buffer's name has.
157///
158/// ```text
159/// *magit:<view>[:<repo>[:<rest>]]*
160///          │       │        └─ whatever the view encodes: a path, a
161///          │       │           revision, a diff scope, a stash index
162///          │       └─ the repository's label — ALWAYS segment 2
163///          └─ which view this is
164/// ```
165///
166/// **Segment 2 is the repository, always.** That rule is what makes the
167/// name parseable at all once views carry both a repository and their
168/// own parameters: `*magit:log:src*` is otherwise unreadable — repo
169/// `src`, or the log of the path `src`? Fixing the position answers it
170/// without asking the record, and the only way to keep the rule true is
171/// to never produce a `rest` without a repo segment before it (hence the
172/// empty segment in [`magit_buffer_name_with`] when there is no repo).
173#[derive(Debug, PartialEq, Eq)]
174pub(crate) struct MagitName<'a> {
175    pub view: &'a str,
176    /// `None` outside a repository, where the trigger has nothing to
177    /// label with. Not an error — the view opens and says so.
178    pub repo: Option<&'a str>,
179    /// The view's own encoded parameters, unsplit: each view knows its
180    /// own shape and parses this itself.
181    pub rest: Option<&'a str>,
182}
183
184/// MR.2/MR.3: the one producer of a magit view's buffer name.
185///
186/// `("status", "lattice")` → `*magit:status:lattice*`. The label comes
187/// from [`repo_label`] (or [`qualified_repo_label`] on a collision), not
188/// from the workdir directly, because the caller — not this function —
189/// is the one that knows whether the plain form is already taken.
190///
191/// One producer and one parser ([`parse_magit_name`]), every caller
192/// through them: MG.15 lost every stash chord to a hand-rolled producer
193/// drifting from its parser, and this name has more callers than that
194/// one did.
195pub(crate) fn magit_buffer_name(view: &str, label: &str) -> String {
196    if label.is_empty() {
197        // The repo-less form: what every magit buffer was called before
198        // MR.2, and still what a trigger outside any repository opens.
199        return format!("*magit:{view}*");
200    }
201    format!("*magit:{view}:{label}*")
202}
203
204/// The same producer for a view that encodes parameters of its own —
205/// a path, a revision, a diff scope.
206///
207/// With no repository the repo segment is left **empty** rather than
208/// omitted (`*magit:diff::staged:src/main.rs*`). Omitting it would slide
209/// `staged` into segment 2 and the parser would report it as the
210/// repository — the ambiguity the fixed position exists to prevent. The
211/// name is admittedly odd, and it is only reachable outside any
212/// repository, where the view's whole content is "Not a git repository."
213pub(crate) fn magit_buffer_name_with(view: &str, label: &str, rest: &str) -> String {
214    format!("*magit:{view}:{label}:{rest}*")
215}
216
217/// MR.3: the parser half — `*magit:<view>[:<repo>[:<rest>]]*` split into
218/// its three parts.
219///
220/// `None` for anything that is not a magit buffer name at all. An empty
221/// repo segment reads back as `None`, which is the round-trip of
222/// [`magit_buffer_name_with`]'s no-repository form.
223pub(crate) fn parse_magit_name(name: &str) -> Option<MagitName<'_>> {
224    let body = name.strip_prefix("*magit:")?.strip_suffix('*')?;
225    let mut parts = body.splitn(3, ':');
226    let view = parts.next().filter(|v| !v.is_empty())?;
227    Some(MagitName {
228        view,
229        repo: parts.next().filter(|r| !r.is_empty()),
230        rest: parts.next().filter(|r| !r.is_empty()),
231    })
232}
233
234/// MR.2: does this name belong to a magit buffer at all?
235///
236/// The first of design §2's three questions — "is the active buffer a
237/// magit buffer" — asked of the only thing a trigger has to hand: the
238/// name. A magit chord pressed inside magit must not change which
239/// repository you are working on.
240pub(crate) fn is_magit_buffer_name(name: &str) -> bool {
241    name.starts_with("*magit:") && name.ends_with('*')
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use std::process::Command;
248
249    fn git_ok(dir: &Path, args: &[&str]) {
250        let st = Command::new("git")
251            .args(args)
252            .current_dir(dir)
253            .status()
254            .expect("git");
255        assert!(st.success(), "git {args:?} failed");
256    }
257
258    /// The bug this module exists to prevent, stated directly: a file
259    /// path handed to the directory-taking discovery finds nothing.
260    ///
261    /// Asserted against `gix` itself rather than against our wrapper,
262    /// because the behaviour being relied on is `gix`'s — if a future
263    /// version starts accepting file paths, this test says so instead
264    /// of the split quietly becoming pointless.
265    #[test]
266    fn discovery_needs_a_directory_which_is_why_the_file_form_is_separate() {
267        let dir = tempfile::tempdir().expect("tempdir");
268        let p = dir.path();
269        git_ok(p, &["init"]);
270        std::fs::write(p.join("a.txt"), "x\n").unwrap();
271
272        assert!(
273            Repository::discover(p).is_ok(),
274            "a directory discovers the repo"
275        );
276        assert!(
277            Repository::discover(p.join("a.txt")).is_err(),
278            "a FILE path does not — and `.ok()` on this is what made \
279             three sites fail silently before MG.11 found them"
280        );
281    }
282
283    #[test]
284    fn workdir_for_file_resolves_the_path_relative_to_the_repository() {
285        let dir = tempfile::tempdir().expect("tempdir");
286        let p = dir.path();
287        git_ok(p, &["init"]);
288        std::fs::create_dir(p.join("src")).unwrap();
289        let file = p.join("src").join("main.rs");
290        std::fs::write(&file, "fn main() {}\n").unwrap();
291
292        let (workdir, rel) = workdir_for_file(&file).expect("inside a repo");
293        assert_eq!(rel, Path::new("src/main.rs"), "repo-relative, not absolute");
294        assert!(
295            file.starts_with(&workdir),
296            "and the workdir must contain it: {workdir:?}"
297        );
298    }
299
300    /// A path with no parent cannot be discovered from, and saying so
301    /// beats discovering from the process's current directory — which
302    /// would silently resolve to whatever repo the editor was launched
303    /// in.
304    #[test]
305    fn a_parentless_path_is_declined_rather_than_guessed() {
306        assert!(workdir_for_file(Path::new("")).is_none());
307    }
308
309    // ── MR.1: the resolver ───────────────────────────────────────
310
311    /// The magit buffer's own repository wins, and it wins over a file
312    /// that would resolve elsewhere. This is the rule that stops a magit
313    /// chord pressed inside magit from switching repositories under you.
314    #[test]
315    fn a_magit_buffers_own_repo_wins_over_everything() {
316        let dir = tempfile::tempdir().expect("tempdir");
317        let p = dir.path();
318        git_ok(p, &["init"]);
319        std::fs::create_dir_all(p.join("src")).unwrap();
320        let file = p.join("src").join("main.rs");
321        std::fs::write(&file, "fn main() {}\n").unwrap();
322
323        let showing = PathBuf::from("/somewhere/else");
324        assert_eq!(
325            repo_for_trigger(Some(showing.clone()), Some(&file), None),
326            Some(showing),
327            "the buffer in front of you decides, not the file underneath it"
328        );
329    }
330
331    /// The point of the change: the active file's repository, not the
332    /// process's. Asserted with a REAL second repo, since the failure
333    /// mode is "silently resolved to the cwd one".
334    #[test]
335    fn the_active_files_repo_beats_the_working_directory() {
336        let dir = tempfile::tempdir().expect("tempdir");
337        let p = dir.path();
338        git_ok(p, &["init"]);
339        std::fs::create_dir_all(p.join("src")).unwrap();
340        let file = p.join("src").join("main.rs");
341        std::fs::write(&file, "fn main() {}\n").unwrap();
342
343        let resolved = repo_for_trigger(None, Some(&file), None).expect("the file is in a repo");
344        // `canonicalize` because a tempdir under /var is a symlink to
345        // /private/var on macOS, and git reports the resolved form —
346        // comparing the raw paths fails for a reason that has nothing to
347        // do with the behaviour under test.
348        assert_eq!(
349            resolved.canonicalize().ok(),
350            p.canonicalize().ok(),
351            "resolved {resolved:?}, wanted the file's own repo"
352        );
353    }
354
355    /// No magit buffer and no file (a scratch buffer, `*messages*`) —
356    /// fall through to the working directory rather than refusing, so a
357    /// fresh editor still answers `C-x g`.
358    #[test]
359    fn with_neither_it_falls_back_to_the_working_directory() {
360        assert_eq!(
361            repo_for_trigger(None, None, None),
362            magit_workdir(),
363            "the fallback IS the old behaviour, unchanged"
364        );
365    }
366
367    /// PR.5: the fallback discovers from where it is TOLD, not from the
368    /// process's working directory.
369    ///
370    /// This is the bug. `:cd` sets `editor.current_dir` and never calls
371    /// `set_current_dir`, so before PR.5 step 3 was
372    /// `Repository::discover(".")` — after `:cd /other/repo`, a fresh
373    /// `C-x g` still opened the repository the editor was *launched* in.
374    #[test]
375    fn the_fallback_discovers_from_the_directory_it_is_given() {
376        let dir = tempfile::tempdir().expect("tempdir");
377        let repo = dir.path().join("elsewhere");
378        std::fs::create_dir_all(&repo).expect("mkdir");
379        std::process::Command::new("git")
380            .arg("init")
381            .current_dir(&repo)
382            .output()
383            .expect("git init");
384        let canonical = std::fs::canonicalize(&repo).expect("canonicalize");
385
386        let got = repo_for_trigger(None, None, Some(&canonical)).expect("a repo was found");
387        assert_eq!(
388            got, canonical,
389            "step 3 must use the directory it was handed, not the process cwd"
390        );
391        // And the pre-PR.5 path is genuinely different — otherwise this
392        // would pass without the fix whenever the suite runs inside a
393        // repository, which it always does.
394        assert_ne!(
395            got,
396            magit_workdir().expect("the suite runs inside a repository"),
397            "the process cwd must not coincidentally be the same repo"
398        );
399    }
400
401    /// A file outside any repository falls through too — it does not
402    /// resolve to nothing, because the editor's own repo is still a
403    /// sensible answer for `C-x g`.
404    #[test]
405    fn a_file_outside_any_repo_falls_through_to_the_working_directory() {
406        let dir = tempfile::tempdir().expect("tempdir");
407        let orphan = dir.path().join("loose.txt");
408        std::fs::write(&orphan, "x\n").unwrap();
409        // No `git init` here on purpose. (If the tempdir happened to sit
410        // inside a repo, discovery would find THAT — which is still the
411        // fall-through behaviour being asserted.)
412        // The claim is the fall-through itself: an unresolvable file
413        // gives the same answer as no file at all, rather than being
414        // more fatal than having opened nothing.
415        assert_eq!(
416            repo_for_trigger(None, Some(&orphan), None),
417            repo_for_trigger(None, None, None),
418            "a file we cannot place must fall through, not refuse"
419        );
420    }
421
422    // ── MR.1: the label ──────────────────────────────────────────
423
424    #[test]
425    fn the_label_is_the_repo_directorys_name() {
426        assert_eq!(repo_label(Path::new("/Users/x/src/lattice")), "lattice");
427        assert_eq!(repo_label(Path::new("/Users/x/work/api")), "api");
428    }
429
430    /// Two checkouts sharing a basename are what the qualified form
431    /// exists for: the same label would otherwise name one buffer for
432    /// two repositories, and the staging chords would act on whichever
433    /// was recorded last.
434    #[test]
435    fn colliding_basenames_qualify_differently() {
436        let a = Path::new("/Users/x/work/api");
437        let b = Path::new("/Users/x/oss/api");
438        assert_eq!(repo_label(a), repo_label(b), "the collision is real");
439        assert_ne!(
440            qualified_repo_label(a),
441            qualified_repo_label(b),
442            "…and qualifying must break it"
443        );
444        assert_eq!(qualified_repo_label(a), "work/api");
445    }
446
447    /// A root path has no final component. Falling back to the whole
448    /// path beats an unnamed buffer.
449    #[test]
450    fn a_rootish_path_still_produces_a_label() {
451        assert!(!repo_label(Path::new("/")).is_empty());
452    }
453
454    /// MR.3b: the working directory is absolute, whatever directory the
455    /// editor was started in.
456    ///
457    /// `gix` reports the workdir relative to where discovery started,
458    /// so running from `crates/lattice-ui-tui` yields `../..` — which
459    /// has no final component and labelled every buffer
460    /// `*magit:<view>:../..*`. This test runs with the cwd at its own
461    /// package directory (cargo's doing), so it exercises exactly that
462    /// case rather than simulating it.
463    #[test]
464    fn the_working_directory_is_absolute_even_from_a_subdirectory() {
465        let Some(workdir) = magit_workdir() else {
466            // Not in a repository at all; nothing to assert.
467            return;
468        };
469        assert!(
470            workdir.is_absolute(),
471            "workdir must be absolute, got {workdir:?}"
472        );
473        let label = repo_label(&workdir);
474        assert!(
475            !label.contains(".."),
476            "a relative workdir leaks into the buffer name: {label}"
477        );
478    }
479
480    // ── MR.2: the naming pair ────────────────────────────────────
481
482    /// Every label the two label functions can produce must survive the
483    /// producer intact — including the qualified form, which contains a
484    /// `/` and is the one a careless parser would split wrongly later.
485    #[test]
486    fn the_producer_puts_the_label_in_the_name() {
487        assert_eq!(
488            magit_buffer_name("status", &repo_label(Path::new("/Users/x/src/lattice"))),
489            "*magit:status:lattice*"
490        );
491        assert_eq!(
492            magit_buffer_name(
493                "status",
494                &qualified_repo_label(Path::new("/Users/x/work/api"))
495            ),
496            "*magit:status:work/api*"
497        );
498    }
499
500    /// The repo-less form is the old name, unchanged — a trigger
501    /// outside any repository must not open `*magit:status:*`, which is
502    /// a name that means nothing and would not match the buffer the
503    /// pre-MR.2 tests and the TUI bindings look for.
504    #[test]
505    fn no_label_produces_the_name_magit_always_had() {
506        assert_eq!(magit_buffer_name("status", ""), "*magit:status*");
507    }
508
509    // ── MR.3: the name grammar ───────────────────────────────────
510
511    /// Everything the producers can make must read back as what went in
512    /// — the property the pair exists for, and the one MG.15 lost.
513    #[test]
514    fn every_name_the_producers_make_the_parser_reads_back() {
515        let cases: &[(String, &str, Option<&str>, Option<&str>)] = &[
516            (
517                magit_buffer_name("status", "lattice"),
518                "status",
519                Some("lattice"),
520                None,
521            ),
522            (
523                magit_buffer_name("status", "work/api"),
524                "status",
525                Some("work/api"),
526                None,
527            ),
528            (magit_buffer_name("status", ""), "status", None, None),
529            (
530                magit_buffer_name_with("diff", "lattice", "staged:src/main.rs"),
531                "diff",
532                Some("lattice"),
533                Some("staged:src/main.rs"),
534            ),
535            (
536                magit_buffer_name_with("file", "api", "a1b2c3d:src/main.rs"),
537                "file",
538                Some("api"),
539                Some("a1b2c3d:src/main.rs"),
540            ),
541        ];
542        for (name, view, repo, rest) in cases {
543            let parsed = parse_magit_name(name).unwrap_or_else(|| panic!("{name} must parse"));
544            assert_eq!(
545                parsed,
546                MagitName {
547                    view,
548                    repo: *repo,
549                    rest: *rest
550                },
551                "{name}"
552            );
553        }
554    }
555
556    /// The rule the fixed position exists for, asserted as the pair of
557    /// names it disambiguates: the repository `src` and the log of the
558    /// path `src` are different buffers, and before MR.3 they were the
559    /// same string.
560    #[test]
561    fn segment_two_is_the_repository_even_when_it_looks_like_a_parameter() {
562        let repo_only = magit_buffer_name("log", "src");
563        let path_in_repo = magit_buffer_name_with("log", "lattice", "src");
564
565        assert_eq!(
566            parse_magit_name(&repo_only).and_then(|n| n.repo),
567            Some("src")
568        );
569        assert_eq!(parse_magit_name(&repo_only).and_then(|n| n.rest), None);
570
571        assert_eq!(
572            parse_magit_name(&path_in_repo).and_then(|n| n.repo),
573            Some("lattice")
574        );
575        assert_eq!(
576            parse_magit_name(&path_in_repo).and_then(|n| n.rest),
577            Some("src")
578        );
579    }
580
581    /// Outside a repository a parameterised view leaves the repo segment
582    /// EMPTY rather than omitting it. Omitting would slide the parameter
583    /// into segment 2 and the parser would report `staged` as the
584    /// repository — which is the whole failure the fixed position
585    /// prevents, arrived at from the producer's side.
586    #[test]
587    fn a_parameterised_name_with_no_repo_keeps_the_slot_open() {
588        let name = magit_buffer_name_with("diff", "", "staged:src/main.rs");
589        let parsed = parse_magit_name(&name).expect("parses");
590        assert_eq!(parsed.repo, None, "there is no repository");
591        assert_eq!(
592            parsed.rest,
593            Some("staged:src/main.rs"),
594            "…and the parameter survives intact: {name}"
595        );
596    }
597
598    #[test]
599    fn names_that_are_not_magit_buffers_do_not_parse() {
600        for name in ["src/main.rs", "*messages*", "*magit:*", "*magit:status"] {
601            assert!(
602                parse_magit_name(name).is_none(),
603                "{name} must not parse as a magit view name"
604            );
605        }
606    }
607
608    #[test]
609    fn magit_buffers_are_recognised_by_name() {
610        assert!(is_magit_buffer_name("*magit:status*"));
611        assert!(is_magit_buffer_name("*magit:status:lattice*"));
612        assert!(is_magit_buffer_name("*magit:diff:staged:src/main.rs*"));
613        assert!(!is_magit_buffer_name("src/main.rs"));
614        assert!(!is_magit_buffer_name("*messages*"));
615    }
616}