{"record":{"id":"b9e84dc1891cbec6","repo":"gitbutlerapp/gitbutler","slug":"bug-it-should-not-be-possible-to-omit-sources","errorCode":null,"errorMessage":"BUG: It should not be possible to omit sources","messagePattern":"BUG: It should not be possible to omit sources","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but/src/command/legacy/move.rs","lineNumber":868,"sourceCode":"            }\n\n            // It doesn't appear as if we need to sort DiffSpecs when they're resolved on a file\n            // level. For the future hunk level DiffSpecs we may need to, however.\n            let changes = NonEmpty::from_vec(builder.into_diff_specs())\n                .expect(\"BUG: Cannot possibly not have any changes here\");\n\n            Ok(ResolvedSources::CommittedChanges((source_commit, changes)))\n        }\n        (None, None, Some(branches)) => {\n            if !branches.tail.is_empty() {\n                Err(bad_input(\"Branches can only be moved one at a time\")\n                    .arg_name(\"<SOURCES>\")\n                    .into())\n            } else {\n                Ok(ResolvedSources::Branch(branches.head))\n            }\n        }\n        (None, None, None) => panic!(\"BUG: It should not be possible to omit sources\"),\n        (_, _, _) => Err(bad_input(\"Mixing source types is not allowed\")\n            .hint(\"You can only move one kind of source (e.g. commits) at a time\")\n            .arg_name(\"<SOURCES>\")\n            .into()),\n    }\n}\n\npub fn run(\n    ctx: &mut Context,\n    meta: &mut impl RefMetadata,\n    perm: &mut RepoExclusive,\n    move_op: MoveOperation,\n) -> anyhow::Result<(MoveOutcome, WorkspaceState)> {\n    let snapshot_details = match &move_op {\n        MoveOperation::CommitsRelativeTo(_) | MoveOperation::CommitsToNewBranch(_) => {\n            SnapshotDetails::new(OperationKind::MoveCommit)\n        }\n        MoveOperation::ChangesRelativeTo(_) | MoveOperation::ChangesToNewBranch(_) => {","sourceCodeStart":850,"sourceCodeEnd":886,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but/src/command/legacy/move.rs#L850-L886","documentation":"`resolve_sources` in but's legacy `move` command matches on which source kinds were detected (committed changes / untracked changes / branches). The `(None, None, None)` arm panics with 'BUG: It should not be possible to omit sources' because the argument parsing layer is expected to reject an empty `<SOURCES>` earlier, so resolution should never see zero sources. Hitting it means an empty or fully-unrecognized SOURCES argument slipped past that earlier validation.","triggerScenarios":"Running the legacy move command with a SOURCES argument that parses to zero recognized items (empty string segments, or revision specs that resolve to nothing); programmatic invocation of the resolve path with empty input.","commonSituations":"Scripts passing an unset/empty shell variable as SOURCES; a CLI parser regression that stops rejecting empty SOURCES; new frontends calling the legacy move internals directly.","solutions":["Pass at least one valid source (commit-ish, branch name, or untracked path) in `<SOURCES>`.","If you hit it from code, add a `bad_input` check that errors when resolved sources are empty instead of dispatching.","Check for trailing whitespace/empty args in the wrapper script producing the command line."],"exampleFix":"// before\nlet sources = resolve_sources(args)?; // panics on (None, None, None)\n\n// after\nlet sources = resolve_sources(args)?;\nif sources.is_empty() {\n    return Err(bad_input(\"At least one <SOURCES> entry is required\")\n        .arg_name(\"<SOURCES>\")\n        .into());\n}","handlingStrategy":"validation","validationCode":"// wrapper guard before invoking the legacy move path\nfn ensure_sources_present(resolved: &ResolvedSources) -> anyhow::Result<()> {\n    if resolved.is_empty() {\n        return Err(bad_input(\"At least one <SOURCES> entry is required\")\n            .arg_name(\"<SOURCES>\")\n            .into());\n    }\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"let sources = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| resolve_sources(&args)))\n    .unwrap_or_else(|_| panic!(\"<SOURCES> resolved to nothing — pass a commit, branch, or untracked path\"));","preventionTips":["Pass non-empty SOURCES arguments; check shell variables for empty expansion before invoking the CLI.","Keep parser-level rejection of empty SOURCES intact when touching argument parsing.","For programmatic callers, validate resolved source counts before dispatching the move operation."],"tags":["rust","cli","move-command","argument-validation","panic"],"backgroundTag":"missing-required-cli-argument","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-23T21:17:23.414Z"}