{"record":{"id":"c2a73e821f56d1ca","repo":"jdx/mise","slug":"dotfiles-duplicate-oci-file-path-path","errorCode":null,"errorMessage":"[dotfiles]: duplicate OCI file path {path:?}","messagePattern":"\\[dotfiles\\]: duplicate OCI file path (.+?)","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/oci/builder.rs","lineNumber":990,"sourceCode":"\n#[derive(Default)]\nstruct DotfilesLayerEntries {\n    files: IndexMap<String, (Vec<u8>, u32)>,\n    dirs: IndexSet<String>,\n}\n\ntype DotfilesLayerFile = (String, Vec<u8>, u32);\ntype DotfilesLayerFiles = Vec<DotfilesLayerFile>;\ntype DotfilesLayerDirs = Vec<String>;\n\nimpl DotfilesLayerEntries {\n    fn add_file(&mut self, path: String, contents: Vec<u8>, mode: u32) -> Result<()> {\n        if self.dirs.contains(&path) {\n            bail!(\"[dotfiles]: duplicate OCI path {path:?} as both file and directory\");\n        }\n        if let Some((existing_contents, existing_mode)) = self.files.get(&path) {\n            if existing_contents != &contents || *existing_mode != mode {\n                bail!(\"[dotfiles]: duplicate OCI file path {path:?}\");\n            }\n            return Ok(());\n        }\n        self.files.insert(path, (contents, mode));\n        Ok(())\n    }\n\n    fn add_dir(&mut self, path: String) -> Result<()> {\n        if self.files.contains_key(&path) {\n            bail!(\"[dotfiles]: duplicate OCI path {path:?} as both file and directory\");\n        }\n        self.dirs.insert(path);\n        Ok(())\n    }\n\n    fn into_layer_inputs(self) -> (DotfilesLayerFiles, DotfilesLayerDirs) {\n        let files = self\n            .files","sourceCodeStart":972,"sourceCodeEnd":1008,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/oci/builder.rs#L972-L1008","documentation":"Thrown while assembling the [dotfiles] OCI layer when two different sources resolve to the same image path with different bytes or modes. add_file is idempotent for identical (contents, mode) pairs, so this fires only on a genuine conflict, not on harmless overlap. It protects the one-path-one-file invariant of the layer before the tar is built.","triggerScenarios":"Two [dotfiles] entries whose normalized targets collide, e.g. [dotfiles.\"~/a\"] target = \"/root/a\" and [dotfiles.\"~/b\"] target = \"~/a\"; or a directory-source walk producing a file at a path another single-file entry already claimed with different contents or a different permission mode.","commonSituations":"Mapping several dotfiles into the same destination (all dumping into ~/), copying the same file twice via different home spellings (~/.gitconfig vs /root/.gitconfig) after editing one source but not the other, or overlapping directory sources where a file changed between walks.","solutions":["Search mise.toml for [dotfiles] entries whose normalized targets (~/x -> root/x, /x -> x) are equal; give each a unique target path.","If the duplicate is intentional (same file referenced twice), make the source files byte-identical and their modes equal, then add_file dedupes silently.","Remove the redundant entry entirely and keep one canonical source for that destination path."],"exampleFix":"# before\n[dotfiles.\"~/.gitconfig-work\"]\ntarget = \"~/.gitconfig\"\n[dotfiles.\"~/.gitconfig-home\"]\ntarget = \"/root/.gitconfig\"  # same image path, different contents\n\n# after\n[dotfiles.\"~/.gitconfig-work\"]\ntarget = \"~/.gitconfig\"\n[dotfiles.\"~/.gitconfig-home\"]\ntarget = \"~/.gitconfig-home\"","handlingStrategy":"validation","validationCode":"# before building, assert no two [dotfiles] targets normalize to the same path\nimport re, sys\nentries = {\"~/.gitconfig\": \"/root/.gitconfig\", \"~/b\": \"~/a\"}  # parsed from mise.toml\ndef norm(t):\n    if t == \"~\": return \"root\"\n    if t.startswith(\"~/\"): return \"root/\" + t[2:]\n    return t.lstrip(\"/\").replace(\"\\\\\", \"/\")\nseen = {}\nfor src, tgt in entries.items():\n    n = norm(tgt)\n    if n in seen and seen[n] != src:\n        sys.exit(f\"duplicate target {n!r} from {seen[n]} and {src}\")\n    seen[n] = src","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Adopt one spelling for home paths in [dotfiles] targets (always ~/...) to avoid accidental collisions.","When multiple sources must land in one directory, give each a distinct file-name suffix under that directory.","Run `mise oci build` in CI on every mise.toml change so target conflicts fail the PR, not a later release build."],"tags":["oci","dotfiles","duplicate-path","config"],"backgroundTag":"duplicate-path-conflict","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}