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}