{"record":{"id":"33235e24474a8c9b","repo":"gitbutlerapp/gitbutler","slug":"conflict-of-was-addressed-more-than-once","errorCode":null,"errorMessage":"Conflict {} of \"{}\" was addressed more than once","messagePattern":"Conflict (.+?) of \"(.+?)\" was addressed more than once","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-api/src/resolve/apply.rs","lineNumber":80,"sourceCode":") -> anyhow::Result<PicksPerFile> {\n    let files_by_path = index_files_by_path(request)?;\n    let mut picks: PicksPerFile = vec![BTreeMap::new(); request.files.len()];\n\n    for spec in specs {\n        let (file_index, hunk_index) = locate_hunk(request, &files_by_path, &spec.path, spec.hunk)?;\n        let file = &request.files[file_index];\n        let pick = match &spec.resolution {\n            HunkResolution::Ours => HunkPick::Ours,\n            HunkResolution::Theirs => HunkPick::Theirs,\n            HunkResolution::Content(content) => {\n                ensure_no_markers(content, &file.path)?;\n                HunkPick::Content(content.clone())\n            }\n            // Replaced with `Content` by `resolve_ai_specs()` before validation.\n            HunkResolution::Ai => bail!(\"AI resolutions must be materialized before validation\"),\n        };\n        if picks[file_index].insert(hunk_index, pick).is_some() {\n            bail!(\n                \"Conflict {} of \\\"{}\\\" was addressed more than once\",\n                spec.hunk,\n                file.path\n            );\n        }\n    }\n\n    Ok(picks)\n}\n\n/// Resolve a `(path, 1-based hunk)` address against the request, returning\n/// the file index and 0-based hunk index.\npub(crate) fn locate_hunk(\n    request: &ResolutionRequest,\n    files_by_path: &BTreeMap<String, usize>,\n    path: &str,\n    hunk: usize,\n) -> anyhow::Result<(usize, usize)> {","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-api/src/resolve/apply.rs#L62-L98","documentation":"Before anything is written, validate_specs() maps each ResolutionSpec to exactly one (file, 1-based hunk) slot in a per-file BTreeMap. Two specs addressing the same hunk of the same path would give one conflict two different resolutions, so the second insert bails and the whole resolve call is rejected atomically (nothing is written to the repo).","triggerScenarios":"Calling resolve_commit_conflict_hunks with specs containing the same {path, hunk} pair twice - e.g. a UI double-submit, a retry that appends to the previous spec list instead of replacing it, or two spellings of the same path ('a/b' and '.\\\\a\\\\b' / 'a//b') that normalize_path maps to the same file.","commonSituations":"Caller accumulates per-checkbox selections and merges arrays from two views; batched 'resolve all with ours' then a per-hunk override both firing; path-format differences (backslashes, leading ./) making the caller's dedupe key miss.","solutions":["Dedupe specs by (normalize_path(path), hunk) before submitting, keeping the intended winner (usually the last)","Fix the double-add in the caller - e.g. a retry path that re-appends already-present specs","Use one canonical path spelling (the exact path returned by commit_conflicts) for every spec"],"exampleFix":"// before\nconst specs = [...selectionA, ...selectionB]; // may contain dup (path, hunk)\nawait api.resolveCommitConflictHunks(commitId, specs);\n\n// after\nconst seen = new Set();\nconst specs = [...selectionA, ...selectionB].filter(s => {\n  const key = `${normalizePath(s.path)}:${s.hunk}`;\n  if (seen.has(key)) return false;\n  seen.add(key);\n  return true;\n});\nawait api.resolveCommitConflictHunks(commitId, specs);","handlingStrategy":"validation","validationCode":"// Dedupe specs by normalized (path, hunk), last one wins\nfunction normalizePath(p: string): string {\n  return p.trim().replace(/\\\\/g, '/').replace(/^\\.\\//, '').replace(/\\/\\+/g, '/');\n}\nconst byKey = new Map<string, Spec>();\nfor (const s of specs) byKey.set(`${normalizePath(s.path)}:${s.hunk}`, s);\nconst uniqueSpecs = [...byKey.values()];","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Key selections by (normalized path, hunk) in the UI state so duplicates cannot accumulate","On retry/double-submit, replace the spec list instead of appending","Use the exact path strings returned by commit_conflicts for every spec"],"tags":["conflict-resolution","duplicate","validation","resolve-api"],"backgroundTag":"duplicate-item-in-request","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}