performance(but): avoid rebuilding entire IdMap to get change ID#14847
Conversation
| .success() | ||
| .stdout_eq(str![[r#" | ||
| Moved 2 commits → after 1#2 | ||
| Moved 2 commits → after 1 |
There was a problem hiding this comment.
Some of these outputs turned a bit weird as they contain collisions, and we're not resolving from the IdMap here. It's mostly fine because collisions aren't that likely, but it's a topic for improvement.
There was a problem hiding this comment.
Pull request overview
This PR addresses a performance regression in the but CLI’s human-formatted output by avoiding expensive IdMap rebuilds when rendering commit identifiers. It introduces a cheaper change-id retrieval path and updates multiple commands (and their snapshots) to use the new commit rendering approach.
Changes:
- Introduces
utils::get_change_id_for_commit()to read a commit’s change-id from headers (or synthesize a deterministic fallback from the commit OID). - Reworks
theme::Committo optionally render using a providedChangeId, removing the previousCommitRef/new_commit_ref*flow that could trigger costlyIdMapconstruction. - Updates legacy and newer command outputs and corresponding snapshot tests to match the new commit-id rendering behavior.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/but/src/theme.rs | Changes commit display to accept an optional change-id and renders a short prefix when present. |
| crates/but/src/utils/object_id.rs | Adds get_change_id_for_commit() helper for retrieving/synthesizing change-ids. |
| crates/but/src/utils/mod.rs | Re-exports the new change-id helper; introduces an (currently empty) change_id module. |
| crates/but/src/utils/change_id.rs | New (empty) module placeholder. |
| crates/but/src/id/mod.rs | Uses the new helper for mapping commit→change-id and moves MIN_DISPLAYED_CHANGE_ID_CHARS usage to theme. |
| crates/but/src/utils/rejection.rs | Updates commit rendering call site to the new theme::Commit(…, Option<ChangeId>) signature. |
| crates/but/src/command/commit/move.rs | Avoids IdMap rebuild by pre-fetching change-ids and using theme::Commit in human output. |
| crates/but/src/command/legacy/absorb.rs | Removes repo-shortening dependency in plan rendering and updates commit display calls. |
| crates/but/src/command/legacy/branch/show.rs | Computes change-ids per commit and uses theme::Commit for human output. |
| crates/but/src/command/legacy/commit.rs | Uses get_change_id_for_commit() to display new commit refs without rebuilding IdMap. |
| crates/but/src/command/legacy/commit2.rs | Updates output to use the new theme::Commit signature (currently without change-ids). |
| crates/but/src/command/legacy/move2.rs | Updates output to use the new theme::Commit signature (currently without change-ids). |
| crates/but/src/command/legacy/reword.rs | Uses a precomputed change-id for display after rewriting a commit. |
| crates/but/src/command/legacy/resolve.rs | Updates commit ref formatting to use theme::Commit directly. |
| crates/but/src/command/legacy/rub/amend.rs | Uses a precomputed change-id for display after rewriting a commit. |
| crates/but/src/command/legacy/rub/mod.rs | Updates multiple operations to use theme::Commit and (sometimes) get_change_id_for_commit(). |
| crates/but/src/command/legacy/rub/squash.rs | Reuses pre-mutation change-ids for displaying the final rewritten commit. |
| crates/but/src/command/legacy/show.rs | Stores object/change ids for display and uses theme::Commit rendering. |
| crates/but/src/command/legacy/squash2.rs | Updates output to use the new theme::Commit signature (currently without change-ids). |
| crates/but/tests/but/command/move.rs | Updates snapshots for move output under the new commit display formatting. |
| crates/but/tests/but/command/absorb.rs | Updates snapshots for absorb output under the new commit display formatting. |
| crates/but/tests/but/command/branch/show.rs | Updates snapshots for branch show output under the new commit display formatting. |
4740df5 to
53995e6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (3)
crates/but/src/command/commit/move.rs:212
- Change IDs are computed from the pre-move OIDs, but the output uses
post_move_id(...)which may differ when the move rewrites commits. If a commit had no stored change-id header,get_change_id_for_commitsynthesizes from the commit OID, so reusing the pre-move value can print a change-id that doesn’t correspond to the post-move commit and can’t be used to refer to it.
let t = theme::get();
if let Some(out) = out.for_human() {
let action = if after { "after" } else { "before" };
if sources.len() == 1 {
let source = theme::Commit(
crates/but/src/command/legacy/rub/amend.rs:37
change_idis computed from the pre-amend commit OID and then reused for the newly created commit. When the original commit lacks a stored change-id header,get_change_id_for_commit()synthesizes from the commit OID, so the amended commit will have a different synthetic change-id and the output will be incorrect.
let change_id = {
let repo = ctx.repo.get()?;
crate::utils::get_change_id_for_commit(&repo, oid)?
};
let (new_commit, rejected) =
crates/but/src/command/legacy/reword.rs:216
- The displayed change-id is computed from the pre-reword commit OID and reused for the rewritten commit. If the original commit had no stored change-id header (so the value is synthesized from the OID), the new commit will have a different synthetic change-id and the output will be wrong.
let change_id = {
let repo = ctx.repo.get()?;
get_change_id_for_commit(&repo, commit_oid)?
};
let new_commit_oid = but_api::commit::reword::commit_reword_with_perm(
Changes to `theme.rs` introduced a relatively significant performance hit to many commands when formatting output for humans. They'd recompute the entire `IdMap` from context, which entails rebuilding the workspace projection. This commit reverts that by reusing pre-mutation change IDs when available, otherwise trying to fetch from an existing `IdMap`, or if that is not available fetching the commit from the repo. This is overall not well structured and we should take more care with the `but2` commands, which I've left unimplemented and deferred for GB-1771.
53995e6 to
aae2c6d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (3)
crates/but/src/theme.rs:650
theme::Committruncates the fullChangeIdto a fixed 3-byte prefix. This ignoresIdMap's disambiguatedshort_id(which can be longer than 3) and drops any#Ncollision suffix, so the rendered commit ref can become ambiguous and diverge from whatbut status/CLI parsing expects.
pub struct Commit(pub gix::ObjectId, pub Option<ChangeId>);
impl Display for Commit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let t = get();
if let Some(change_id) = &self.1 {
let s = change_id
.get(..MIN_DISPLAYED_CHANGE_ID_CHARS.min(change_id.len()))
.unwrap_or_default();
write!(f, "{}", t.change_id.paint(s.to_str_lossy()))
} else {
write!(
f,
"{}",
t.commit_id.paint(self.0.to_hex_with_len(7).to_string())
)
}
crates/but/src/command/legacy/reword.rs:226
change_idis captured from the pre-reword commit and reused for rendering the new commit ID. If the original commit has no explicit Change-Id header,get_change_id_for_commit()synthesizes an ID from the old commit OID, which will differ after rewriting—so the printed ID can be wrong for the new commit. Compute the change-id fromnew_commit_oid.new_commit(or only reuse the explicit header value).
let change_id = {
let repo = ctx.repo.get()?;
get_change_id_for_commit(&repo, commit_oid)?
};
let new_commit_oid = but_api::commit::reword::commit_reword_with_perm(
ctx,
commit_oid,
BString::from(new_message),
DryRun::No,
perm,
)?;
if let Some(out) = out.for_human() {
let new_commit = crate::theme::Commit(new_commit_oid.new_commit, Some(change_id));
writeln!(out, "Updated commit message for {new_commit}")?;
crates/but/src/command/legacy/rub/amend.rs:43
change_idis captured from the pre-amend commit OID (oid) and reused to render the rewritten commit ID. If the target commit has no explicit Change-Id header, the synthesized ID is derived from the old commit OID and will not match the new commit after rewriting. Compute the change-id from the resultingnew_commitOID (or only reuse the explicit header value).
let change_id = {
let repo = ctx.repo.get()?;
crate::utils::get_change_id_for_commit(&repo, oid)?
};
let (new_commit, rejected) =
amend_diff_specs(ctx, diff_specs, oid, target_branch.as_deref(), perm)?;
update_workspace_commit(ctx, false)?;
if let Some(out) = out.for_human() {
let new_commit = new_commit
.map(|id| theme::Commit(id, Some(change_id)).to_string())
.unwrap_or_default();
Changes to
theme.rsintroduced a relatively significant performance hit to many commands when formatting output for humans. They'd recompute the entireIdMapfrom context, which entails rebuilding the workspace projection. I measured a 5-8% performance hit tobut commitfrom this computation, which is too much for just output formatting.This commit somewhat reverts that by reusing pre-mutation change IDs when available, otherwise trying to fetch from an existing
IdMap, or if that is not available fetching the commit from the repo.For the most part, we're trusting that change IDs stay the same. This is overall not well structured and we should take more care with the
but2commands, which I've left unimplemented and deferred for GB-1771.