{"record":{"id":"2f9a5c05240e3948","repo":"gitbutlerapp/gitbutler","slug":"has-conflict-but-conflict-was-addres","errorCode":null,"errorMessage":"\"{}\" has {} conflict{}, but conflict {} was addressed","messagePattern":"\"(.+?)\" has (.+?) conflict(.+?), but conflict (.+?) was addressed","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-api/src/resolve/apply.rs","lineNumber":120,"sourceCode":"            // Naming the reason matters here: the file *is* conflicted, it just\n            // has no hunks to address, so \"not a conflicted file\" would read as\n            // a caller mistake rather than a property of the conflict.\n            let normalized = normalize_path(path);\n            match request\n                .manual\n                .iter()\n                .find(|file| normalize_path(&file.path) == normalized)\n            {\n                Some(file) => format!(\n                    \"\\\"{path}\\\" cannot be resolved this way: {} Resolve this commit in edit mode instead.\",\n                    file.reason\n                ),\n                None => format!(\"\\\"{path}\\\" is not a conflicted file of this commit\"),\n            }\n        })?;\n    let file = &request.files[file_index];\n    if hunk == 0 || hunk > file.hunks.len() {\n        bail!(\n            \"\\\"{}\\\" has {} conflict{}, but conflict {} was addressed\",\n            file.path,\n            file.hunks.len(),\n            if file.hunks.len() == 1 { \"\" } else { \"s\" },\n            hunk\n        );\n    }\n    Ok((file_index, hunk - 1))\n}\n\n/// Map normalized request paths to file indices, rejecting collisions.\npub(crate) fn index_files_by_path(\n    request: &ResolutionRequest,\n) -> anyhow::Result<BTreeMap<String, usize>> {\n    let mut files_by_path = BTreeMap::new();\n    for (index, file) in request.files.iter().enumerate() {\n        if files_by_path\n            .insert(normalize_path(&file.path), index)","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-api/src/resolve/apply.rs#L102-L138","documentation":"locate_hunk() resolves each spec's (path, 1-based hunk) address against the request. Hunks are numbered starting at 1, so hunk 0 is always invalid, and a number larger than the file's conflict count is stale. The message tells you the file's real hunk count so you can recompute. Nothing is written when this fails.","triggerScenarios":"Calling resolve_commit_conflict_hunks with hunk numbers computed from a DIFFERENT view of the commit: a previous partial resolution rewrote the commit (new id, fewer hunks), the UI held an old conflict list, or the caller passed 0-based indices from some other diff/hunk API.","commonSituations":"Stale UI state after a partial resolve (must re-fetch against the returned new_commit); two concurrent resolutions racing; mixing 0-based indices from a git-diff style API with this 1-based API.","solutions":["Re-fetch the conflict state with commit_conflicts(commit_id) and recompute spec hunk numbers from its fresh hunks list","After any partial resolution, address follow-up specs to the new_commit id returned by that call","Treat hunk indices as 1-based positions in the file's hunk list"],"exampleFix":"// before: reusing stale hunk numbers after a partial resolve\nawait api.resolveCommitConflictHunks(commitId, specs);\n\n// after: re-sync against the current commit first\nconst conflicts = await api.commitConflicts(commitId);\nconst file = conflicts.files.find(f => normalizePath(f.path) === normalizePath(spec.path));\nconst safeSpecs = file\n  ? specs.filter(s => s.path === file.path && s.hunk >= 1 && s.hunk <= file.hunks.length)\n  : [];\nawait api.resolveCommitConflictHunks(commitId, safeSpecs);","handlingStrategy":"validation","validationCode":"// Recompute against the live conflict list before resolving\nconst conflicts = await api.commitConflicts(commitId);\nconst file = conflicts.files.find(f => normalizePath(f.path) === normalizePath(spec.path));\nconst ok = file !== undefined && spec.hunk >= 1 && spec.hunk <= file.hunks.length;\nif (!ok) throw new Error(`stale hunk ${spec.hunk} for ${spec.path}; refresh conflicts`);","typeGuard":null,"tryCatchPattern":"try {\n  await api.resolveCommitConflictHunks(commitId, specs);\n} catch (err) {\n  if (/but conflict \\d+ was addressed/.test(String(err))) {\n    const fresh = await api.commitConflicts(commitId);\n    specs = remapSpecsToFreshHunks(specs, fresh); // rebuild indices, then retry once\n    await api.resolveCommitConflictHunks(commitId, specs);\n  } else throw err;\n}","preventionTips":["Treat hunk indices as 1-based positions in commit_conflicts().files[i].hunks","After every resolve, adopt the returned new_commit as the id for subsequent operations","Disable batch apply while a resolve is in flight so two callers cannot invalidate each other's indices"],"tags":["conflict-resolution","index-out-of-range","stale-state","resolve-api"],"backgroundTag":"index-out-of-bounds","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}