{"record":{"id":"079dcfc5c7071b42","repo":"gitbutlerapp/gitbutler","slug":"no-commit-ids-provided-for-uncommit","errorCode":null,"errorMessage":"no commit IDs provided for uncommit","messagePattern":"no commit IDs provided for uncommit","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-api/src/commit/uncommit.rs","lineNumber":206,"sourceCode":"///\n/// The commits are removed from branch history, but their changes are\n/// **kept** — they surface as uncommitted workspace modifications. When\n/// `assign_to` is set, newly surfaced hunks are assigned to that stack.\n///\n/// This contrasts with [`super::discard_commit::commit_discard()`], which\n/// removes both the commit and its changes.\n///\n/// When `dry_run` is enabled, it returns a preview of the resulting workspace\n/// state without materializing the rewrite.\npub fn commit_uncommit_only_with_perm(\n    ctx: &mut but_ctx::Context,\n    subject_commit_ids: Vec<gix::ObjectId>,\n    assign_to: Option<but_core::ref_metadata::StackId>,\n    dry_run: DryRun,\n    perm: &mut RepoExclusive,\n) -> anyhow::Result<UncommitResult> {\n    if subject_commit_ids.is_empty() {\n        anyhow::bail!(\"no commit IDs provided for uncommit\");\n    }\n    let context_lines = ctx.settings.context_lines;\n    let mut meta = ctx.meta()?;\n    let (repo, mut ws, mut db) = ctx.workspace_mut_and_db_mut_with_perm(perm)?;\n\n    let surfaced =\n        SurfacedHunks::record_before(assign_to, dry_run, &mut db, &repo, &ws, context_lines)?;\n\n    let editor = Editor::create(&mut ws, &mut meta, &repo, &mut db)?;\n\n    let mut rebase =\n        but_workspace::commit::discard_commits(editor, subject_commit_ids.iter().copied())\n            .with_context(|| {\n                format!(\n                    \"failed to uncommit commits: {}\",\n                    subject_commit_ids\n                        .iter()\n                        .map(|id| id.to_hex().to_string())","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but-api/src/commit/uncommit.rs#L188-L224","documentation":"commit_uncommit_only_with_perm refuses an empty subject_commit_ids list before touching the workspace: uncommit needs at least one commit id to rewrite/remove, so the guard bails immediately, before recording surfaced hunks or creating the Editor.","triggerScenarios":"Calling the uncommit API (commit_uncommit_only_with_perm or its but-api wrapper) with an empty Vec of commit ids — empty UI selection, or a caller looping over a commit list that came back empty.","commonSituations":"Frontends not gating the uncommit button on selection; scripts filtering commits by message/path that match nothing.","solutions":["Pass at least one commit id to uncommit.","Guard the caller: skip or notify when the selection is empty instead of calling the API.","Require a non-empty selection in the UI before enabling uncommit."],"exampleFix":"// before\nbut_api::commit_uncommit_only_with_perm(ctx, vec![], assign_to, DryRun::No, perm)?;\n\n// after\nif !subject_commit_ids.is_empty() {\n    but_api::commit_uncommit_only_with_perm(ctx, subject_commit_ids, assign_to, DryRun::No, perm)?;\n}","handlingStrategy":"validation","validationCode":"fn can_uncommit(subject_commit_ids: &[gix::ObjectId]) -> bool {\n    !subject_commit_ids.is_empty()\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Gate the uncommit action on a non-empty selection.","Treat empty commit lists as no-ops in pipelines.","Log when a filter produces zero commits so callers can skip early."],"tags":["uncommit","commits","input-validation","api"],"backgroundTag":"empty-commit-selection","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}