OXYGENHUB
◆ v0.1.0 oxygen / crates / oxy-core / src / repo / prompt.rs
23.3 KiB · Rust · last changed A command parses the manifest once 1 month ago source blame history raw
1
//! Fast, side-effect-free status for shell prompts.
2
//!
3
//! Every snapshot and checkout writes a manifest of the working copy as it
4
//! was last seen: `size`, `mtime`, and BLAKE3 of each file. `quick_status`
5
//! walks the working copy and compares against that manifest, reading a
6
//! file's content only when its size or mtime changed, and records no
7
//! operation. Ahead/behind counts come from walking the store between the
8
//! local bookmark and its `origin` tracking state.
9
10
use super::*;
11
use std::time::UNIX_EPOCH;
12
13
#[derive(Debug, Clone, Default, PartialEq, Eq)]
14
pub struct PromptInfo {
15
    pub bookmark: String,
16
    /// Working-copy ChangeId prefix.
17
    pub change_id: String,
18
    pub heads: usize,
19
    pub dirty: bool,
20
    pub conflicted: bool,
21
    /// Changes local `main` has that `origin/main` lacks, when tracking exists.
22
    pub ahead: Option<usize>,
23
    pub behind: Option<usize>,
24
}
25
26
impl PromptInfo {
27
    /// The glyph for the working copy's state: `‼` conflicted, `✎` dirty,
28
    /// `✔` clean.
29
    pub fn state_glyph(&self) -> &'static str {
30
        if self.conflicted {
31
            "‼"
32
        } else if self.dirty {
33
            "✎"
34
        } else {
35
            "✔"
36
        }
37
    }
38
39
    /// The ANSI colour (SGR parameter) for the state: red, yellow, or cyan.
40
    pub fn state_color(&self) -> u8 {
41
        if self.conflicted {
42
            31
43
        } else if self.dirty {
44
            33
45
        } else {
46
            36
47
        }
48
    }
49
50
    /// `format` wrapped in the state colour. `guard` is `("\\[", "\\]")` for
51
    /// bash prompts, `("%{", "%}")` for zsh, or empty for raw output.
52
    pub fn format_colored(&self, fmt: &str, guard: (&str, &str)) -> String {
53
        format!(
54
            "{}\x1b[{}m{}{}{}\x1b[0m{}",
55
            guard.0,
56
            self.state_color(),
57
            guard.1,
58
            self.format(fmt),
59
            guard.0,
60
            guard.1
61
        )
62
    }
63
64
    /// Expand `%b` bookmark, `%c` change id, `%h` heads, `%d` dirty marker
65
    /// (`*`), `%x` conflict marker (`!`), `%a`/`%B` ahead/behind counts
66
    /// (empty when unknown), `%A`/`%D` `+n`/`-n` when non-zero, `%s` the
67
    /// state glyph, `%U`/`%O` `↑n`/`↓n` when non-zero, `%i` the repository
68
    /// icon, `%%` percent.
69
    pub fn format(&self, fmt: &str) -> String {
70
        let mut out = String::new();
71
        let mut chars = fmt.chars();
72
        while let Some(c) = chars.next() {
73
            if c != '%' {
74
                out.push(c);
75
                continue;
76
            }
77
            match chars.next() {
78
                Some('b') => out.push_str(&self.bookmark),
79
                Some('c') => out.push_str(&self.change_id),
80
                Some('h') => out.push_str(&self.heads.to_string()),
81
                Some('d') => {
82
                    if self.dirty {
83
                        out.push('*');
84
                    }
85
                }
86
                Some('x') => {
87
                    if self.conflicted {
88
                        out.push('!');
89
                    }
90
                }
91
                Some('a') => out.push_str(&self.ahead.map(|n| n.to_string()).unwrap_or_default()),
92
                Some('B') => out.push_str(&self.behind.map(|n| n.to_string()).unwrap_or_default()),
93
                Some('A') => {
94
                    if let Some(n) = self.ahead.filter(|n| *n > 0) {
95
                        out.push_str(&format!("+{n}"));
96
                    }
97
                }
98
                Some('D') => {
99
                    if let Some(n) = self.behind.filter(|n| *n > 0) {
100
                        out.push_str(&format!("-{n}"));
101
                    }
102
                }
103
                Some('s') => out.push_str(self.state_glyph()),
104
                Some('U') => {
105
                    if let Some(n) = self.ahead.filter(|n| *n > 0) {
106
                        out.push_str(&format!("↑{n}"));
107
                    }
108
                }
109
                Some('O') => {
110
                    if let Some(n) = self.behind.filter(|n| *n > 0) {
111
                        out.push_str(&format!("↓{n}"));
112
                    }
113
                }
114
                Some('i') => out.push_str(ICON),
115
                Some('%') => out.push('%'),
116
                Some(other) => {
117
                    out.push('%');
118
                    out.push(other);
119
                }
120
                None => out.push('%'),
121
            }
122
        }
123
        out
124
    }
125
}
126
127
/// Path → (size, mtime in nanoseconds, BLAKE3 hex) as last seen.
128
/// One manifest entry: the file's stat, its content hash, and the stored
129
/// blob for an opaque file.
130
#[derive(Clone, Debug)]
131
pub(crate) struct ManifestEntry {
132
    pub size: u64,
133
    pub mtime_ns: u128,
134
    pub hash: String,
135
    pub blob: Option<ObjectRef>,
136
    /// The graph key (`State::graph_key`) of the base file this text file
137
    /// was found equal to, rendered, byte for byte.
138
    pub graph: Option<String>,
139
    /// Whether the file was executable: a mode change alters neither size
140
    /// nor time.
141
    pub exec: bool,
142
}
143
144
#[derive(Clone, Debug, Default)]
145
pub(crate) struct Manifest {
146
    /// When the manifest was written, for the racy-stat guard.
147
    pub written_ns: u128,
148
    pub entries: BTreeMap<String, ManifestEntry>,
149
}
150
151
/// A parsed manifest and the identity (size, time, inode) of the file it
152
/// came from: reused while the file is the same one.
153
pub(crate) type CachedManifest = ((u64, u128, u64), (bool, bool, std::sync::Arc<Manifest>));
154
155
pub(crate) fn mtime_ns(m: &fs::Metadata) -> u128 {
156
    m.modified()
157
        .ok()
158
        .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
159
        .map(|d| d.as_nanos())
160
        .unwrap_or(0)
161
}
162
163
pub const DEFAULT_PROMPT_FORMAT: &str = "%b %c%d%x%A%D";
164
/// The repository icon for `%i` (an atom, for oxygen).
165
pub const ICON: &str = "⚛";
166
/// A format with glyphs: icon, bookmark, change id, state, ahead/behind.
167
pub const GLYPH_PROMPT_FORMAT: &str = "%i %b %c %s%U%O";
168
169
/// One manifest line per file: size, mtime in nanoseconds, hash, blob ref
170
/// or `-`, graph key or `-`, `x` when executable or `-`, path.
171
fn manifest_line(
172
    path: &str,
173
    size: u64,
174
    mtime_ns: u128,
175
    hash: &str,
176
    blob: Option<ObjectRef>,
177
    graph: Option<&str>,
178
    exec: bool,
179
) -> String {
180
    format!(
181
        "{size}\t{mtime_ns}\t{hash}\t{}\t{}\t{}\t{path}\n",
182
        blob.map(|b| b.to_hex()).unwrap_or_else(|| "-".into()),
183
        graph.unwrap_or("-"),
184
        if exec { "x" } else { "-" }
185
    )
186
}
187
188
impl Repo {
189
    pub(crate) fn manifest_path(&self) -> PathBuf {
190
        self.oxy
191
            .join("workspaces")
192
            .join(self.workspace.to_hex())
193
            .join("manifest")
194
    }
195
196
    /// Record the working copy as it is now (after a snapshot, commit, or
197
    /// checkout), with header lines saying whether the base state is
198
    /// conflicted and whether the working-copy change holds uncommitted work.
199
    pub(crate) fn write_manifest(&self, conflicted: bool, uncommitted: bool) -> Result<()> {
200
        let entries = self.walk_working_copy()?;
201
        self.write_manifest_entries(conflicted, uncommitted, &entries, &HashMap::new())
202
    }
203
204
    /// Write the manifest from entries already walked; `blobs` names the
205
    /// stored blob of each opaque path so the next walk can skip reading it.
206
    pub(crate) fn write_manifest_entries(
207
        &self,
208
        conflicted: bool,
209
        uncommitted: bool,
210
        entries: &BTreeMap<String, FsEntry>,
211
        blobs: &HashMap<String, ObjectRef>,
212
    ) -> Result<()> {
213
        self.manifest_cache.take();
214
        let written = std::time::SystemTime::now()
215
            .duration_since(UNIX_EPOCH)
216
            .map(|d| d.as_nanos())
217
            .unwrap_or(0);
218
        let head = format!(
219
            "# conflicted={}\n# uncommitted={}\n",
220
            conflicted as u8, uncommitted as u8
221
        );
222
        let mut body = String::new();
223
        let mut newest_ns: u128 = 0;
224
        for (path, e) in entries {
225
            let full = self.root.join(path);
226
            let (size, mtime_ns) = match e.stat {
227
                Some(known) => known,
228
                None => match fs::symlink_metadata(&full) {
229
                    Ok(m) => (m.len(), mtime_ns(&m)),
230
                    Err(_) => (e.content.len() as u64, 0),
231
                },
232
            };
233
            let hash = match &e.content_hash {
234
                Some(h) => h.clone(),
235
                None => blake3::hash(&e.content).to_hex().to_string(),
236
            };
237
            let blob = blobs.get(path).copied().or(e.blob);
238
            newest_ns = newest_ns.max(mtime_ns);
239
            body.push_str(&manifest_line(
240
                path,
241
                size,
242
                mtime_ns,
243
                &hash,
244
                blob,
245
                e.graph_key.as_deref(),
246
                e.exec,
247
            ));
248
        }
249
        let p = self.manifest_path();
250
        // Leave an unchanged manifest alone when its stamp already trusts
251
        // every entry (none is within the racy window): rewriting it would
252
        // change nothing but the file, which editors watching the tree
253
        // (and subrepos inside it) take as a change.
254
        if let Ok(old) = fs::read_to_string(&p) {
255
            let old_written: Option<u128> = old.lines().find_map(|l| {
256
                l.strip_prefix("# written=")
257
                    .and_then(|v| v.trim().parse().ok())
258
            });
259
            let old_rest: String = old
260
                .lines()
261
                .filter(|l| !l.starts_with("# written="))
262
                .map(|l| format!("{l}\n"))
263
                .collect();
264
            if let Some(w) = old_written {
265
                if old_rest == format!("{head}{body}") && newest_ns + 2_000_000_000 < w {
266
                    return Ok(());
267
                }
268
            }
269
        }
270
        let text = format!("{head}# written={written}\n{body}");
271
        fs::create_dir_all(p.parent().unwrap())?;
272
        write_atomic(&p, text.as_bytes())
273
    }
274
275
    /// Change the header flags, and set the graph key of each path in
276
    /// `keys` (after a commit, the new base's keys of the files it holds as
277
    /// they are on disk).
278
    pub(crate) fn rewrite_manifest(
279
        &self,
280
        conflicted: bool,
281
        uncommitted: bool,
282
        keys: &HashMap<String, String>,
283
    ) -> Result<()> {
284
        self.manifest_cache.take();
285
        let p = self.manifest_path();
286
        let text = match fs::read_to_string(&p) {
287
            Ok(t) => t,
288
            Err(_) => return self.write_manifest(conflicted, uncommitted),
289
        };
290
        let mut out = String::with_capacity(text.len());
291
        for line in text.lines() {
292
            if line.starts_with("# conflicted=") {
293
                out.push_str(&format!("# conflicted={}\n", conflicted as u8));
294
            } else if line.starts_with("# uncommitted=") {
295
                out.push_str(&format!("# uncommitted={}\n", uncommitted as u8));
296
            } else {
297
                let f: Vec<&str> = line.splitn(7, '\t').collect();
298
                match (f.len() == 7).then(|| keys.get(f[6])).flatten() {
299
                    Some(k) => out.push_str(&format!(
300
                        "{}\t{}\t{}\t{}\t{k}\t{}\t{}\n",
301
                        f[0], f[1], f[2], f[3], f[5], f[6]
302
                    )),
303
                    None => {
304
                        out.push_str(line);
305
                        out.push('\n');
306
                    }
307
                }
308
            }
309
        }
310
        write_atomic(&p, out.as_bytes())
311
    }
312
313
    /// Forget the manifest after an operation that changed the working-copy
314
    /// change without a snapshot (undo, rewrites); the prompt shows blank
315
    /// markers until the next snapshot.
316
    pub(crate) fn drop_manifest(&self) {
317
        self.manifest_cache.take();
318
        let _ = fs::remove_file(self.manifest_path());
319
    }
320
321
    /// The manifest, with whether it records a conflict and uncommitted
322
    /// work; parsed once while the file stays the same one.
323
    pub(crate) fn read_manifest(&self) -> Result<Option<(bool, bool, std::sync::Arc<Manifest>)>> {
324
        let path = self.manifest_path();
325
        let id = match fs::metadata(&path) {
326
            Ok(m) => {
327
                #[cfg(unix)]
328
                let ino = std::os::unix::fs::MetadataExt::ino(&m);
329
                #[cfg(not(unix))]
330
                let ino = 0;
331
                (m.len(), mtime_ns(&m), ino)
332
            }
333
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
334
            Err(e) => return Err(e.into()),
335
        };
336
        if let Some((cached_id, m)) = &*self.manifest_cache.borrow() {
337
            if *cached_id == id {
338
                return Ok(Some(m.clone()));
339
            }
340
        }
341
        let Some((c, u, m)) = self.parse_manifest(&path)? else {
342
            return Ok(None);
343
        };
344
        let m = (c, u, std::sync::Arc::new(m));
345
        *self.manifest_cache.borrow_mut() = Some((id, m.clone()));
346
        Ok(Some(m))
347
    }
348
349
    fn parse_manifest(&self, path: &std::path::Path) -> Result<Option<(bool, bool, Manifest)>> {
350
        let text = match fs::read_to_string(path) {
351
            Ok(t) => t,
352
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
353
            Err(e) => return Err(e.into()),
354
        };
355
        let mut out = Manifest::default();
356
        let mut conflicted = false;
357
        let mut uncommitted = false;
358
        for line in text.lines() {
359
            if let Some(v) = line.strip_prefix("# conflicted=") {
360
                conflicted = v.trim() == "1";
361
                continue;
362
            }
363
            if let Some(v) = line.strip_prefix("# uncommitted=") {
364
                uncommitted = v.trim() == "1";
365
                continue;
366
            }
367
            if let Some(v) = line.strip_prefix("# written=") {
368
                out.written_ns = v.trim().parse().unwrap_or(u128::MAX);
369
                continue;
370
            }
371
            // A line of another shape (an older manifest) is no entry: the
372
            // file is then read, and the next snapshot rewrites the line.
373
            let f: Vec<&str> = line.splitn(7, '\t').collect();
374
            if f.len() == 7 {
375
                out.entries.insert(
376
                    f[6].to_string(),
377
                    ManifestEntry {
378
                        size: f[0].parse().unwrap_or(0),
379
                        mtime_ns: f[1].parse().unwrap_or(0),
380
                        hash: f[2].to_string(),
381
                        blob: ObjectRef::from_hex(f[3]),
382
                        graph: (f[4] != "-").then(|| f[4].to_string()),
383
                        exec: f[5] == "x",
384
                    },
385
                );
386
            }
387
        }
388
        Ok(Some((conflicted, uncommitted, out)))
389
    }
390
391
    /// Whether the working copy is certainly clean, known from the manifest
392
    /// alone, without loading any history (spec §12.4): the last snapshot
393
    /// or commit found no uncommitted work and no conflict, there are no
394
    /// subrepos (whose state the manifest does not hold), and no file was
395
    /// added, removed or changed since. An entry inside the racy window,
396
    /// where an equal size and time do not prove equal content, is checked
397
    /// by hashing the file. `false` means only that a snapshot must decide.
398
    pub fn clean_by_manifest(&self) -> Result<bool> {
399
        let _t = crate::timing::phase("status: check the manifest");
400
        let Some((conflicted, uncommitted, m)) = self.read_manifest()? else {
401
            return Ok(false);
402
        };
403
        if conflicted || uncommitted || self.root.join(".oxymodules").exists() {
404
            return Ok(false);
405
        }
406
        for (rel, e) in &m.entries {
407
            if e.mtime_ns + 2_000_000_000 < m.written_ns {
408
                continue;
409
            }
410
            let path = self.root.join(rel);
411
            let same = match fs::symlink_metadata(&path) {
412
                Ok(md) if md.file_type().is_symlink() => fs::read_link(&path).is_ok_and(|t| {
413
                    blake3::hash(t.to_string_lossy().as_bytes())
414
                        .to_hex()
415
                        .as_str()
416
                        == e.hash
417
                }),
418
                Ok(md) if md.is_file() => {
419
                    md.len() == e.size
420
                        && fs::read(&path)
421
                            .is_ok_and(|b| blake3::hash(&b).to_hex().as_str() == e.hash)
422
                }
423
                _ => false,
424
            };
425
            if !same {
426
                return Ok(false);
427
            }
428
        }
429
        Ok(self.quick_dirty()? == Some(false))
430
    }
431
432
    /// Whether the working copy differs from the last manifest. Without a
433
    /// manifest the answer is `None`.
434
    pub fn quick_dirty(&self) -> Result<Option<bool>> {
435
        let Some((_, uncommitted, manifest)) = self.read_manifest()? else {
436
            return Ok(None);
437
        };
438
        if uncommitted {
439
            return Ok(Some(true));
440
        }
441
        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
442
        let nested = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
443
        let wb = self.walker(&nested);
444
        // Every file is checked on its own, so the walk runs in parallel and
445
        // stops at the first difference.
446
        let dirty = AtomicBool::new(false);
447
        let seen = AtomicUsize::new(0);
448
        let (root, sparse) = (&self.root, &self.config.sparse_paths);
449
        let in_scope = |p: &str| {
450
            sparse.is_empty()
451
                || sparse
452
                    .iter()
453
                    .any(|s| p == s || p.starts_with(&format!("{s}/")))
454
        };
455
        wb.build_parallel().run(|| {
456
            Box::new(|entry| {
457
                use ignore::WalkState;
458
                if dirty.load(Relaxed) {
459
                    return WalkState::Quit;
460
                }
461
                let Ok(entry) = entry else {
462
                    return WalkState::Continue;
463
                };
464
                let Some(ft) = entry.file_type() else {
465
                    return WalkState::Continue;
466
                };
467
                if ft.is_dir() {
468
                    return WalkState::Continue;
469
                }
470
                let under = entry.path().strip_prefix(root).unwrap();
471
                let rel: String = match under.to_str() {
472
                    Some(s) if std::path::MAIN_SEPARATOR == '/' => s.to_string(),
473
                    _ => under
474
                        .components()
475
                        .map(|c| c.as_os_str().to_string_lossy().to_string())
476
                        .collect::<Vec<_>>()
477
                        .join("/"),
478
                };
479
                if !in_scope(&rel) {
480
                    return WalkState::Continue;
481
                }
482
                // Whether this file differs from its entry; an unreadable
483
                // one counts as different.
484
                let differs = match manifest.entries.get(&rel) {
485
                    None => true,
486
                    Some(me) => {
487
                        seen.fetch_add(1, Relaxed);
488
                        match fs::symlink_metadata(entry.path()) {
489
                            Err(_) => true,
490
                            Ok(_) if ft.is_symlink() => {
491
                                !fs::read_link(entry.path()).is_ok_and(|t| {
492
                                    blake3::hash(t.to_string_lossy().as_bytes())
493
                                        .to_hex()
494
                                        .as_str()
495
                                        == me.hash
496
                                })
497
                            }
498
                            Ok(m) => {
499
                                m.len() != me.size
500
                                    || super::is_executable(&m) != me.exec
501
                                    || (mtime_ns(&m) != me.mtime_ns
502
                                        && !fs::read(entry.path()).is_ok_and(|b| {
503
                                            blake3::hash(&b).to_hex().as_str() == me.hash
504
                                        }))
505
                            }
506
                        }
507
                    }
508
                };
509
                if differs {
510
                    dirty.store(true, Relaxed);
511
                    return WalkState::Quit;
512
                }
513
                WalkState::Continue
514
            })
515
        });
516
        if dirty.load(Relaxed) {
517
            return Ok(Some(true));
518
        }
519
        let seen = seen.load(Relaxed);
520
        if seen == manifest.entries.len() {
521
            return Ok(Some(false));
522
        }
523
        // Manifest entries the walk skipped: tracked files that ignore rules
524
        // hide. They count as present when still on disk and unchanged.
525
        for (rel, me) in &manifest.entries {
526
            let path = self.root.join(rel);
527
            let Ok(m) = fs::symlink_metadata(&path) else {
528
                return Ok(Some(true));
529
            };
530
            if m.file_type().is_symlink() {
531
                continue;
532
            }
533
            if m.len() != me.size
534
                || super::is_executable(&m) != me.exec
535
                || (mtime_ns(&m) != me.mtime_ns
536
                    && blake3::hash(&fs::read(&path)?).to_hex().as_str() != me.hash)
537
            {
538
                return Ok(Some(true));
539
            }
540
        }
541
        Ok(Some(false))
542
    }
543
544
    /// Prompt information without recording anything.
545
    pub fn quick_status(&self) -> Result<PromptInfo> {
546
        let view = self.view()?;
547
        let bookmark = self.config.bookmark.clone();
548
        let heads = self.heads_of(&view, &bookmark)?;
549
        let wc_hash = view.workspaces.get(&self.workspace).copied();
550
        let change_id = match wc_hash {
551
            Some(h) => self.store.get_change_meta(&h)?.change_id.short(8),
552
            None => String::new(),
553
        };
554
        // Conflictedness was recorded by the last snapshot; without a
555
        // manifest (never snapshotted) both flags are unknown and shown clear.
556
        let manifest = self.read_manifest()?;
557
        let conflicted = manifest.as_ref().map(|(c, _, _)| *c).unwrap_or(false);
558
        let dirty = self.quick_dirty()?.unwrap_or(false);
559
        let (ahead, behind) = match view.remotes.get("origin").and_then(|b| b.get(&bookmark)) {
560
            Some(st) => {
561
                let remote_heads = self.store.get_state(st)?.heads;
562
                let ahead = self.negotiate(&remote_heads, &heads).map(|v| v.len()).ok();
563
                let behind = self.negotiate(&heads, &remote_heads).map(|v| v.len()).ok();
564
                (ahead, behind)
565
            }
566
            None => (None, None),
567
        };
568
        Ok(PromptInfo {
569
            bookmark,
570
            change_id,
571
            heads: heads.len(),
572
            dirty,
573
            conflicted,
574
            ahead,
575
            behind,
576
        })
577
    }
578
}
579
580
#[cfg(test)]
581
mod tests {
582
    use super::*;
583
584
    #[test]
585
    fn placeholders_and_glyphs() {
586
        let clean = PromptInfo {
587
            bookmark: "main".into(),
588
            change_id: "658990aa".into(),
589
            heads: 1,
590
            dirty: false,
591
            conflicted: false,
592
            ahead: Some(0),
593
            behind: None,
594
        };
595
        assert_eq!(clean.format(DEFAULT_PROMPT_FORMAT), "main 658990aa");
596
        assert_eq!(clean.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✔");
597
        let busy = PromptInfo {
598
            dirty: true,
599
            ahead: Some(2),
600
            behind: Some(3),
601
            ..clean.clone()
602
        };
603
        assert_eq!(busy.format(DEFAULT_PROMPT_FORMAT), "main 658990aa*+2-3");
604
        assert_eq!(busy.format(GLYPH_PROMPT_FORMAT), "⚛ main 658990aa ✎↑2↓3");
605
        let bad = PromptInfo {
606
            conflicted: true,
607
            ..busy.clone()
608
        };
609
        assert_eq!(bad.format("%s%x%d %h %a/%B %%"), "‼!* 1 2/3 %");
610
        assert_eq!(bad.state_color(), 31);
611
        assert_eq!(
612
            busy.format_colored("%b", ("\\[", "\\]")),
613
            "\\[\x1b[33m\\]main\\[\x1b[0m\\]"
614
        );
615
        assert_eq!(clean.format_colored("%b", ("", "")), "\x1b[36mmain\x1b[0m");
616
        assert_eq!(clean.format("%q"), "%q");
617
    }
618
}