Skip to main content

grit_lib/
refs_fsck.rs

1//! Reference database consistency checks for `git refs verify` and `git fsck --references`.
2//!
3//! Aligns with Git's `refs_fsck` / `files_fsck_*` and `packed_fsck` behavior and message text.
4
5use std::cmp::Ordering;
6use std::fs;
7use std::io;
8use std::path::{Component, Path, PathBuf};
9
10use crate::check_ref_format::{check_refname_format, RefNameOptions};
11use crate::config::ConfigSet;
12use crate::objects::ObjectId;
13use crate::odb::Odb;
14use crate::repo::Repository;
15
16/// Severity of a refs-fsck diagnostic.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum RefsFsckSeverity {
19    Error,
20    Warning,
21}
22
23/// One diagnostic (use [`format_refs_fsck_line`] for Git-compatible output).
24#[derive(Debug, Clone)]
25pub struct RefsFsckIssue {
26    pub severity: RefsFsckSeverity,
27    pub path: String,
28    pub msg_id: &'static str,
29    pub detail: String,
30}
31
32/// `error: path: msgId: detail` / `warning: ...`
33#[must_use]
34pub fn format_refs_fsck_line(issue: &RefsFsckIssue) -> String {
35    let level = match issue.severity {
36        RefsFsckSeverity::Error => "error",
37        RefsFsckSeverity::Warning => "warning",
38    };
39    format!(
40        "{}: {}: {}: {}",
41        level, issue.path, issue.msg_id, issue.detail
42    )
43}
44
45fn canonical_git_dir(git_dir: &Path) -> PathBuf {
46    let commondir_file = git_dir.join("commondir");
47    let Some(raw) = fs::read_to_string(commondir_file).ok() else {
48        return git_dir.to_path_buf();
49    };
50    let rel = raw.trim();
51    if rel.is_empty() {
52        return git_dir.to_path_buf();
53    }
54    let path = if Path::new(rel).is_absolute() {
55        PathBuf::from(rel)
56    } else {
57        git_dir.join(rel)
58    };
59    path.canonicalize().unwrap_or(path)
60}
61
62fn is_pseudo_ref(name: &str) -> bool {
63    matches!(name, "FETCH_HEAD" | "MERGE_HEAD" | "ORIG_HEAD")
64}
65
66fn is_root_ref_syntax(name: &str) -> bool {
67    !name.is_empty()
68        && name
69            .bytes()
70            .all(|b| b.is_ascii_uppercase() || b == b'-' || b == b'_')
71}
72
73/// Matches Git's `is_root_ref` closely enough for fsck root ref enumeration.
74fn is_root_ref(name: &str) -> bool {
75    if !is_root_ref_syntax(name) || is_pseudo_ref(name) {
76        return false;
77    }
78    if name.ends_with("_HEAD") {
79        return true;
80    }
81    matches!(
82        name,
83        "HEAD"
84            | "AUTO_MERGE"
85            | "BISECT_EXPECTED_REV"
86            | "NOTES_MERGE_PARTIAL"
87            | "NOTES_MERGE_REF"
88            | "MERGE_AUTOSTASH"
89    )
90}
91
92fn stripped_for_head_check(display_path: &str) -> &str {
93    display_path
94        .strip_prefix("worktrees/")
95        .and_then(|s| s.find('/').map(|i| &s[i + 1..]))
96        .unwrap_or(display_path)
97}
98
99fn ref_path_for_name_check(display_path: &str) -> &str {
100    if let Some(rest) = display_path.strip_prefix("worktrees/") {
101        if let Some(idx) = rest.find("/refs/") {
102            return &rest[idx + 1..];
103        }
104        if rest.ends_with("/HEAD") || rest == "HEAD" {
105            return "HEAD";
106        }
107    }
108    display_path
109}
110
111fn fsck_refs_msg_severity(
112    config: &ConfigSet,
113    camel_id: &str,
114    default_warn: bool,
115    strict: bool,
116) -> Option<RefsFsckSeverity> {
117    let key = format!("fsck.{camel_id}");
118    let v = config.get(&key).map(|s| s.to_ascii_lowercase());
119    if matches!(v.as_deref(), Some("ignore")) {
120        return None;
121    }
122    let level = match v.as_deref() {
123        Some("warn") => RefsFsckSeverity::Warning,
124        Some("error") => RefsFsckSeverity::Error,
125        _ => {
126            if default_warn {
127                if strict {
128                    RefsFsckSeverity::Error
129                } else {
130                    RefsFsckSeverity::Warning
131                }
132            } else {
133                RefsFsckSeverity::Error
134            }
135        }
136    };
137    Some(level)
138}
139
140fn push_issue(
141    issues: &mut Vec<RefsFsckIssue>,
142    config: &ConfigSet,
143    strict: bool,
144    camel_id: &'static str,
145    default_warn: bool,
146    path: String,
147    detail: String,
148) {
149    let Some(sev) = fsck_refs_msg_severity(config, camel_id, default_warn, strict) else {
150        return;
151    };
152    issues.push(RefsFsckIssue {
153        severity: sev,
154        path,
155        msg_id: camel_id,
156        detail,
157    });
158}
159
160/// Run ref database checks (files backend + packed-refs). `strict` is `git refs verify --strict`.
161pub fn refs_fsck(
162    repo: &Repository,
163    odb: &Odb,
164    config: &ConfigSet,
165    strict: bool,
166) -> io::Result<Vec<RefsFsckIssue>> {
167    let mut issues = Vec::new();
168    let common = canonical_git_dir(&repo.git_dir);
169
170    let mut stores: Vec<(PathBuf, Option<String>)> = vec![(common.clone(), None)];
171    let worktrees_dir = common.join("worktrees");
172    if let Ok(rd) = fs::read_dir(&worktrees_dir) {
173        for e in rd.flatten() {
174            if e.file_type().map(|t| t.is_dir()).unwrap_or(false) {
175                let id = e.file_name().to_string_lossy().to_string();
176                stores.push((e.path(), Some(id)));
177            }
178        }
179    }
180
181    for (git_dir, wt_id) in stores {
182        fsck_worktree(
183            &git_dir,
184            wt_id.as_deref(),
185            &common,
186            odb,
187            config,
188            strict,
189            &mut issues,
190        )?;
191    }
192
193    // Preserve discovery order (matches Git). Do not sort: message order matters for the same
194    // path (e.g. `symlinkRef` before `badReferentName`), and `packed-refs line N` sorts
195    // incorrectly as strings (`line 10` before `line 2`). Aggregate tests use `sort` on output.
196    Ok(issues)
197}
198
199fn fsck_worktree(
200    git_dir: &Path,
201    worktree_id: Option<&str>,
202    common_dir: &Path,
203    odb: &Odb,
204    config: &ConfigSet,
205    strict: bool,
206    issues: &mut Vec<RefsFsckIssue>,
207) -> io::Result<()> {
208    let refs_dir = git_dir.join("refs");
209    if refs_dir.is_dir() {
210        walk_refs_files(common_dir, &refs_dir, odb, config, strict, issues)?;
211    }
212
213    if worktree_id.is_none() {
214        fsck_packed_refs(common_dir, config, strict, issues)?;
215    }
216
217    fsck_root_refs(
218        git_dir,
219        common_dir,
220        path_prefix_for_root(worktree_id),
221        odb,
222        config,
223        strict,
224        issues,
225    )?;
226    Ok(())
227}
228
229fn path_prefix_for_root(worktree_id: Option<&str>) -> Option<String> {
230    worktree_id.map(|id| format!("worktrees/{id}/"))
231}
232
233fn display_rel_path(common_dir: &Path, path: &Path) -> String {
234    let rel = path.strip_prefix(common_dir).unwrap_or(path);
235    rel.to_string_lossy().replace('\\', "/")
236}
237
238/// Resolve `base.join(rel)` with `..` collapsed (Git-style symlink target handling when the
239/// destination path does not exist and `canonicalize` fails).
240fn normalize_joined_path(base: &Path, rel: &Path) -> PathBuf {
241    let combined = base.join(rel);
242    let mut out = PathBuf::new();
243    for comp in combined.components() {
244        match comp {
245            Component::Prefix(p) => out.push(p.as_os_str()),
246            Component::RootDir => out.push(Component::RootDir.as_os_str()),
247            Component::CurDir => {}
248            Component::ParentDir => {
249                let _ = out.pop();
250            }
251            Component::Normal(p) => out.push(p),
252        }
253    }
254    out
255}
256
257fn walk_refs_files(
258    common_dir: &Path,
259    dir: &Path,
260    odb: &Odb,
261    config: &ConfigSet,
262    strict: bool,
263    issues: &mut Vec<RefsFsckIssue>,
264) -> io::Result<()> {
265    for entry in fs::read_dir(dir)? {
266        let entry = entry?;
267        let path = entry.path();
268        let fname = entry.file_name().to_string_lossy().to_string();
269        if fname == "." || fname == ".." {
270            continue;
271        }
272        if path.is_dir() {
273            walk_refs_files(common_dir, &path, odb, config, strict, issues)?;
274            continue;
275        }
276        if !path.is_file() && !path.is_symlink() {
277            continue;
278        }
279        if !fname.starts_with('.') && fname.ends_with(".lock") {
280            continue;
281        }
282        let display = display_rel_path(common_dir, &path);
283        verify_loose_ref(common_dir, &display, &path, odb, config, strict, issues)?;
284    }
285    Ok(())
286}
287
288fn fsck_root_refs(
289    git_dir: &Path,
290    common_dir: &Path,
291    path_prefix: Option<String>,
292    odb: &Odb,
293    config: &ConfigSet,
294    strict: bool,
295    issues: &mut Vec<RefsFsckIssue>,
296) -> io::Result<()> {
297    let Ok(rd) = fs::read_dir(git_dir) else {
298        return Ok(());
299    };
300    for entry in rd.flatten() {
301        let name = entry.file_name().to_string_lossy().to_string();
302        if name.starts_with('.') {
303            continue;
304        }
305        if !name.starts_with('.') && name.ends_with(".lock") {
306            continue;
307        }
308        if !is_root_ref(&name) {
309            continue;
310        }
311        let path = entry.path();
312        let meta = match fs::symlink_metadata(&path) {
313            Ok(m) => m,
314            Err(_) => continue,
315        };
316        if !meta.is_file() && !meta.is_symlink() {
317            continue;
318        }
319        let display = match &path_prefix {
320            Some(p) => format!("{p}{name}"),
321            None => name,
322        };
323        verify_loose_ref(common_dir, &display, &path, odb, config, strict, issues)?;
324    }
325    Ok(())
326}
327
328fn verify_loose_ref(
329    common_dir: &Path,
330    display_path: &str,
331    path: &Path,
332    odb: &Odb,
333    config: &ConfigSet,
334    strict: bool,
335    issues: &mut Vec<RefsFsckIssue>,
336) -> io::Result<()> {
337    check_ref_file_name(display_path, config, strict, issues);
338
339    let meta = fs::symlink_metadata(path)?;
340    if !meta.is_file() && !meta.is_symlink() {
341        push_issue(
342            issues,
343            config,
344            strict,
345            "badRefFiletype",
346            false,
347            display_path.to_owned(),
348            "unexpected file type".to_owned(),
349        );
350        return Ok(());
351    }
352
353    if meta.is_symlink() {
354        push_issue(
355            issues,
356            config,
357            strict,
358            "symlinkRef",
359            true,
360            display_path.to_owned(),
361            "use deprecated symbolic link for symref".to_owned(),
362        );
363        let target = fs::read_link(path)?;
364        let parent = path.parent().unwrap_or(Path::new(""));
365        let joined = normalize_joined_path(parent, Path::new(&target));
366        let resolved = fs::canonicalize(&joined).unwrap_or(joined);
367        let abs_common = fs::canonicalize(common_dir).unwrap_or(common_dir.to_path_buf());
368        let g = abs_common.to_string_lossy();
369        let r = resolved.to_string_lossy().to_string();
370        let referent = if r.starts_with(g.as_ref()) {
371            let rest = &r[g.len()..];
372            rest.trim_start_matches(['/', '\\']).replace('\\', "/")
373        } else {
374            r.replace('\\', "/")
375        };
376        refs_fsck_symref(display_path, &referent, config, strict, issues);
377        return Ok(());
378    }
379
380    let raw = fs::read_to_string(path)?;
381    let buf = raw.strip_suffix('\r').unwrap_or(&raw);
382
383    if let Some(after) = buf.strip_prefix("ref:") {
384        let mut s = after;
385        while s
386            .as_bytes()
387            .first()
388            .is_some_and(|b| b.is_ascii_whitespace())
389        {
390            s = &s[1..];
391        }
392        fsck_symref_contents(display_path, s, config, strict, issues);
393        return Ok(());
394    }
395
396    let bytes = buf.as_bytes();
397    let mut i = 0usize;
398    while i < bytes.len() && bytes[i].is_ascii_hexdigit() {
399        i += 1;
400    }
401    if i != 40 {
402        push_issue(
403            issues,
404            config,
405            strict,
406            "badRefContent",
407            false,
408            display_path.to_owned(),
409            buf.trim_end_matches(['\n', '\r']).to_owned(),
410        );
411        return Ok(());
412    }
413    let oid: ObjectId = match buf[..40].parse() {
414        Ok(o) => o,
415        Err(_) => {
416            push_issue(
417                issues,
418                config,
419                strict,
420                "badRefContent",
421                false,
422                display_path.to_owned(),
423                buf.trim_end_matches(['\n', '\r']).to_owned(),
424            );
425            return Ok(());
426        }
427    };
428    let trailing = &buf[40..];
429    if !trailing.is_empty()
430        && !trailing
431            .as_bytes()
432            .first()
433            .is_some_and(|b| b.is_ascii_whitespace())
434    {
435        push_issue(
436            issues,
437            config,
438            strict,
439            "badRefContent",
440            false,
441            display_path.to_owned(),
442            buf.trim_end_matches(['\n', '\r']).to_owned(),
443        );
444        return Ok(());
445    }
446
447    if trailing.is_empty() {
448        push_issue(
449            issues,
450            config,
451            strict,
452            "refMissingNewline",
453            true,
454            display_path.to_owned(),
455            "misses LF at the end".to_owned(),
456        );
457    } else if trailing != "\n" {
458        // Git: warn when `*trailing != '\n' || *(trailing + 1)` — only a lone `\n` after the oid
459        // is valid; anything else (including `\n\n\n`) is reported with the full tail string.
460        push_issue(
461            issues,
462            config,
463            strict,
464            "trailingRefContent",
465            true,
466            display_path.to_owned(),
467            format!("has trailing garbage: '{trailing}'"),
468        );
469    }
470
471    if oid.is_zero() {
472        push_issue(
473            issues,
474            config,
475            strict,
476            "badRefOid",
477            false,
478            display_path.to_owned(),
479            format!("points to invalid object ID '{}'", oid.to_hex()),
480        );
481    } else if !odb.exists(&oid) {
482        push_issue(
483            issues,
484            config,
485            strict,
486            "missingObject",
487            false,
488            display_path.to_owned(),
489            format!("points to missing object {}", oid.to_hex()),
490        );
491    }
492
493    Ok(())
494}
495
496fn check_ref_file_name(
497    display_path: &str,
498    config: &ConfigSet,
499    strict: bool,
500    issues: &mut Vec<RefsFsckIssue>,
501) {
502    let check_path = ref_path_for_name_check(display_path);
503    if is_root_ref(check_path) || check_path == "HEAD" {
504        return;
505    }
506    if check_refname_format(
507        check_path,
508        &RefNameOptions {
509            allow_onelevel: false,
510            refspec_pattern: false,
511            normalize: false,
512        },
513    )
514    .is_err()
515    {
516        push_issue(
517            issues,
518            config,
519            strict,
520            "badRefName",
521            false,
522            display_path.to_owned(),
523            "invalid refname format".to_owned(),
524        );
525    }
526}
527
528fn fsck_symref_contents(
529    display_path: &str,
530    referent_raw: &str,
531    config: &ConfigSet,
532    strict: bool,
533    issues: &mut Vec<RefsFsckIssue>,
534) {
535    // Match `files_fsck_symref_target` + `strbuf_rtrim` (trim trailing ASCII whitespace).
536    let orig_len = referent_raw.len();
537    let orig_last_byte = referent_raw.as_bytes().last().copied();
538    let trimmed = referent_raw.trim_end_matches(|c: char| c.is_ascii_whitespace());
539    let after_len = trimmed.len();
540
541    if after_len == orig_len || (after_len < orig_len && orig_last_byte != Some(b'\n')) {
542        push_issue(
543            issues,
544            config,
545            strict,
546            "refMissingNewline",
547            true,
548            display_path.to_owned(),
549            "misses LF at the end".to_owned(),
550        );
551    }
552    if after_len != orig_len && after_len != orig_len.saturating_sub(1) {
553        push_issue(
554            issues,
555            config,
556            strict,
557            "trailingRefContent",
558            true,
559            display_path.to_owned(),
560            "has trailing whitespaces or newlines".to_owned(),
561        );
562    }
563
564    refs_fsck_symref(display_path, trimmed, config, strict, issues);
565}
566
567fn refs_fsck_symref(
568    display_path: &str,
569    target: &str,
570    config: &ConfigSet,
571    strict: bool,
572    issues: &mut Vec<RefsFsckIssue>,
573) {
574    let stripped = stripped_for_head_check(display_path);
575    if stripped == "HEAD" && !target.starts_with("refs/heads/") {
576        push_issue(
577            issues,
578            config,
579            strict,
580            "badHeadTarget",
581            false,
582            display_path.to_owned(),
583            format!("HEAD points to non-branch '{target}'"),
584        );
585    }
586
587    if is_root_ref(target) {
588        return;
589    }
590
591    if check_refname_format(
592        target,
593        &RefNameOptions {
594            allow_onelevel: false,
595            refspec_pattern: false,
596            normalize: false,
597        },
598    )
599    .is_err()
600    {
601        push_issue(
602            issues,
603            config,
604            strict,
605            "badReferentName",
606            false,
607            display_path.to_owned(),
608            format!("points to invalid refname '{target}'"),
609        );
610        return;
611    }
612
613    if !target.starts_with("refs/") && !target.starts_with("worktrees/") {
614        push_issue(
615            issues,
616            config,
617            strict,
618            "symrefTargetIsNotARef",
619            true,
620            display_path.to_owned(),
621            format!("points to non-ref target '{target}'"),
622        );
623    }
624}
625
626fn cmp_packed_refname(r1: &str, r2: &str) -> Ordering {
627    let b1 = r1.as_bytes();
628    let b2 = r2.as_bytes();
629    let mut i = 0;
630    loop {
631        let c1 = b1.get(i).copied();
632        let c2 = b2.get(i).copied();
633        match (c1, c2) {
634            (None, None) => return Ordering::Equal,
635            (Some(b'\n'), None) => return Ordering::Less,
636            (None, Some(b'\n')) => return Ordering::Greater,
637            (Some(b'\n'), Some(b'\n')) => return Ordering::Equal,
638            (Some(b'\n'), _) => return Ordering::Less,
639            (_, Some(b'\n')) => return Ordering::Greater,
640            (Some(a), Some(b)) if a != b => return a.cmp(&b),
641            (Some(_), Some(_)) => i += 1,
642            (None, Some(_)) => return Ordering::Less,
643            (Some(_), None) => return Ordering::Greater,
644        }
645    }
646}
647
648fn fsck_packed_refs(
649    common_dir: &Path,
650    config: &ConfigSet,
651    strict: bool,
652    issues: &mut Vec<RefsFsckIssue>,
653) -> io::Result<()> {
654    let path = common_dir.join("packed-refs");
655    let meta = match fs::symlink_metadata(&path) {
656        Ok(m) => m,
657        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
658        Err(e) => return Err(e),
659    };
660    if meta.is_symlink() {
661        push_issue(
662            issues,
663            config,
664            strict,
665            "badRefFiletype",
666            false,
667            "packed-refs".to_owned(),
668            "not a regular file but a symlink".to_owned(),
669        );
670        return Ok(());
671    }
672    if !meta.is_file() {
673        push_issue(
674            issues,
675            config,
676            strict,
677            "badRefFiletype",
678            false,
679            "packed-refs".to_owned(),
680            "not a regular file".to_owned(),
681        );
682        return Ok(());
683    }
684    let data = fs::read(&path)?;
685    if data.is_empty() {
686        push_issue(
687            issues,
688            config,
689            strict,
690            "emptyPackedRefsFile",
691            true,
692            "packed-refs".to_owned(),
693            "file is empty".to_owned(),
694        );
695        return Ok(());
696    }
697
698    let text = String::from_utf8_lossy(&data).into_owned();
699    let mut sorted = false;
700    let mut main_ref_order: Vec<(usize, String)> = Vec::new();
701
702    for (line_no, raw_line) in text.lines().enumerate() {
703        let line_no = line_no + 1;
704        let line = raw_line.trim_end_matches('\r');
705
706        if line_no == 1 && line.starts_with('#') {
707            if line.starts_with("# pack-refs with: ") {
708                let traits = line
709                    .strip_prefix("# pack-refs with: ")
710                    .unwrap_or("")
711                    .split_whitespace();
712                sorted = traits.clone().any(|t| t == "sorted");
713            } else if line.contains("pack-refs") {
714                push_issue(
715                    issues,
716                    config,
717                    strict,
718                    "badPackedRefHeader",
719                    false,
720                    "packed-refs.header".to_owned(),
721                    format!("'{line}' does not start with '# pack-refs with: '"),
722                );
723            }
724            continue;
725        }
726
727        if line.is_empty() || line.starts_with('#') {
728            continue;
729        }
730
731        if let Some(inner) = line.strip_prefix('^') {
732            let mut j = 0usize;
733            while j < inner.len() && inner.as_bytes()[j].is_ascii_hexdigit() {
734                j += 1;
735            }
736            if j != 40 {
737                push_issue(
738                    issues,
739                    config,
740                    strict,
741                    "badPackedRefEntry",
742                    false,
743                    format!("packed-refs line {line_no}"),
744                    format!("'{inner}' has invalid peeled oid"),
745                );
746            } else if j < inner.len() {
747                push_issue(
748                    issues,
749                    config,
750                    strict,
751                    "badPackedRefEntry",
752                    false,
753                    format!("packed-refs line {line_no}"),
754                    format!("has trailing garbage after peeled oid '{}'", &inner[40..]),
755                );
756            }
757            continue;
758        }
759
760        let mut j = 0usize;
761        while j < line.len() && line.as_bytes()[j].is_ascii_hexdigit() {
762            j += 1;
763        }
764        let oid_hex = &line[..j];
765        let rest = &line[j..];
766
767        if oid_hex.len() != 40 {
768            let display_line = format!("{oid_hex}{rest}");
769            push_issue(
770                issues,
771                config,
772                strict,
773                "badPackedRefEntry",
774                false,
775                format!("packed-refs line {line_no}"),
776                format!("'{display_line}' has invalid oid"),
777            );
778            continue;
779        }
780
781        if rest.is_empty()
782            || !rest
783                .as_bytes()
784                .first()
785                .is_some_and(|b| b.is_ascii_whitespace())
786        {
787            push_issue(
788                issues,
789                config,
790                strict,
791                "badPackedRefEntry",
792                false,
793                format!("packed-refs line {line_no}"),
794                format!(
795                    "has no space after oid '{oid_hex}' but with '{}'",
796                    rest.trim_end_matches('\r')
797                ),
798            );
799            continue;
800        }
801
802        // Skip the single separator whitespace after the oid (Git: `p++` after `isspace`).
803        let rest = rest.trim_end_matches('\r');
804        let refname = match rest.chars().next() {
805            Some(c) if c.is_whitespace() => &rest[c.len_utf8()..],
806            _ => rest,
807        };
808
809        if check_refname_format(
810            refname,
811            &RefNameOptions {
812                allow_onelevel: false,
813                refspec_pattern: false,
814                normalize: false,
815            },
816        )
817        .is_err()
818        {
819            push_issue(
820                issues,
821                config,
822                strict,
823                "badRefName",
824                false,
825                format!("packed-refs line {line_no}"),
826                format!("has bad refname '{refname}'"),
827            );
828        }
829
830        main_ref_order.push((line_no, refname.to_owned()));
831    }
832
833    if sorted && main_ref_order.len() >= 2 {
834        let mut former: Option<&str> = None;
835        for (line_no, refname) in &main_ref_order {
836            if let Some(prev) = former {
837                if cmp_packed_refname(refname, prev) != Ordering::Greater {
838                    push_issue(
839                        issues,
840                        config,
841                        strict,
842                        "packedRefUnsorted",
843                        false,
844                        format!("packed-refs line {line_no}"),
845                        format!("refname '{refname}' is less than previous refname '{prev}'"),
846                    );
847                    break;
848                }
849            }
850            former = Some(refname.as_str());
851        }
852    }
853
854    Ok(())
855}