Skip to content

fix(branch-tag-reset): close review gaps#334

Open
genedna wants to merge 9 commits intoweb3infra-foundation:mainfrom
genedna:main
Open

fix(branch-tag-reset): close review gaps#334
genedna wants to merge 9 commits intoweb3infra-foundation:mainfrom
genedna:main

Conversation

@genedna
Copy link
Copy Markdown
Member

@genedna genedna commented Apr 1, 2026

No description provided.

Signed-off-by: Quanyi Ma <eli@patch.sh>
Copilot AI review requested due to automatic review settings April 1, 2026 14:35
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 210205d877

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes review gaps for the branch, tag, and reset commands by tightening typed error handling, unifying run/render execution paths, and expanding regression coverage for output/help contracts.

Changes:

  • Introduces/expands typed error enums and centralized run_* + render_* flows for branch, tag, and reset.
  • Adds human-mode confirmation output and JSON contract refinements (while keeping JSON-only fields stable via skip_serializing).
  • Expands CLI integration tests for confirmations, JSON nullability, help EXAMPLES, and corruption/error-code surfacing.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/command/tag_test.rs Adds coverage for JSON message: null on lightweight tags and human confirmation lines.
tests/command/reset_test.rs Adds regression test ensuring subtree corruption during pathspec reset maps to repo-corrupt error behavior.
tests/command/output_flags_test.rs Ensures --help output includes an EXAMPLES: section for branch/reset/tag.
tests/command/branch_test.rs Adds coverage for branch create/delete confirmations, suggestions, and config write failure surfacing (unix).
src/internal/branch.rs Adds BranchStoreError + result-returning APIs; updates parsing to avoid panics on corrupt stored data.
src/command/tag.rs Adds TagError, unifies execution via run_tag and rendering via render_tag_output, adds help examples.
src/command/reset.rs Adds ResetError, typed propagation through helpers, warning aggregation via emit_warning, adds help examples.
src/command/branch.rs Adds BranchError, unifies run/render path, adds fuzzy “did you mean” suggestions and help examples.
docs/improvement/tag.md Updates status/plan narrative for tag improvements.
docs/improvement/reset.md Updates status/plan narrative for reset improvements.
docs/improvement/README.md Updates overall improvement roadmap status table.
docs/improvement/branch.md Updates status/plan narrative for branch improvements.

Comment on lines +139 to 146
pub async fn list_branches_with_conn<C>(db: &C, remote: Option<&str>) -> Vec<Self>
where
C: ConnectionTrait,
{
Self::list_branches_result_with_conn(db, remote)
.await
.unwrap_or_default()
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list_branches_with_conn() now converts any BranchStoreError into an empty branch list via unwrap_or_default(). This can silently mask DB/query corruption and cause callers like Branch::list_branches() (used by fetch/log/remote/switch/etc.) to behave as if there are zero branches rather than surfacing an actionable error.

Consider either (a) updating the non-*_result* APIs (list_branches / list_branches_with_conn) to return Result<Vec<Branch>, BranchStoreError> and migrating callers, or (b) at least emitting a tracing::error!/warning when falling back to default so the failure is observable.

Copilot uses AI. Check for mistakes.
Signed-off-by: Quanyi Ma <eli@patch.sh>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown
Member Author

@genedna genedna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive Code Review — PR #334

Overall Assessment

This is a well-structured refactoring PR that modernizes three commands (branch, tag, reset) with typed error enums, run/render separation, and structured JSON output. The changes are consistent with the patterns established in earlier batches (switch/checkout, bisect). The internal/branch.rs fallible API additions are a welcome improvement over the old unwrap()-based code paths.


Code Quality

Strengths:

  • Excellent adoption of the thiserror error enum pattern across all three commands (BranchError, TagError, ResetError), with well-structured From<XError> for CliError impls that centralize error-to-exit-code mapping.
  • Clean run/render separation: run_branch()render_branch_output() pattern eliminates the old JSON-vs-text branching in execute_safe.
  • The new BranchStoreError in internal/branch.rs replaces unwrap() calls on DB queries with proper error propagation — a significant robustness improvement.
  • Good use of #[serde(skip_serializing)] for display-only fields (display_name, display_message) that shouldn't leak into JSON.
  • Levenshtein fuzzy matching for branch-not-found suggestions is a nice UX touch.

Issues flagged inline:

  1. Bug: set_upstream_with_conn idempotency check compares config.merge against the short branch name instead of the full refs/heads/... ref — the optimization never triggers.
  2. Non-atomic rename: rename_branch_impl creates the new branch, updates HEAD, then deletes the old branch without a transaction wrapper.

Security

  • No security vulnerabilities identified. Input validation is handled via clap's argument parsing.
  • The split_once('/') upstream parsing correctly rejects inputs without a slash via BranchError::InvalidUpstream.
  • No user-controlled strings are interpolated into SQL — all DB operations go through Sea-ORM's parameterized queries.

Performance

  • The idempotency check in set_upstream_with_conn (if fixed) would save unnecessary DB writes.
  • branch_not_found_error loads all branches to compute Levenshtein suggestions. For repositories with thousands of branches, this could be slow. Consider capping the iteration or only computing suggestions when the branch list is below a threshold.
  • collect_branch_output does separate queries for local and remote branches, then filters both — this is reasonable for the expected scale.

Testing

Strengths:

  • Good coverage of new behaviors: test_branch_create_outputs_confirmation, test_branch_not_found_suggests_similar_name, test_branch_force_delete_outputs_confirmation.
  • The #[cfg(unix)] test for config-write failure via read-only DB permissions is creative and tests a real failure mode.
  • test_reset_pathspec_surfaces_subtree_corruption_as_repo_corrupt is excellent — it corrupts a real object on disk and verifies the new ResetError::ObjectLoad path.
  • Tag tests verify the lightweight-vs-annotated JSON contract (message is null for lightweight tags).
  • output_flags_test.rs additions verify --help includes EXAMPLES sections for all three commands.

Suggestions:

  • No test for the set_upstream_with_conn idempotency path (currently broken). After the fix, add a test that sets upstream twice and verifies the second call is a no-op.
  • No test for rename_branch_impl — consider adding one that verifies HEAD is updated when renaming the current branch.

Documentation

  • BRANCH_AFTER_HELP, RESET_EXAMPLES, and TAG_EXAMPLES constants provide useful CLI examples including agent/JSON usage patterns.
  • The docs/improvement/ updates accurately reflect the landed state vs. remaining work items.
  • The improvement docs are thorough and well-organized with clear "landed" vs "remaining" tracking.

Summary of Action Items

Priority File Issue
High branch.rs:435 Fix idempotency check: config.merge != merge_ref instead of config.merge != remote_branch
Medium branch.rs:617-630 Consider transactional rename or document non-atomicity
Low reset.rs:270,290 Log get_commit_summary errors instead of silent unwrap_or_default()
Low reset.rs:443 Log rollback warnings before discarding
Low tag.rs:287 / branch.rs:330 Extract shared short_hash/abbreviate_hash utility

Good work overall — the typed error approach is paying dividends in readability and testability. The main issue to address is the idempotency bug in set_upstream_with_conn.


Generated by Claude Code

let merge_ref = format!("refs/heads/{remote_branch}");
let should_write = branch_config
.as_ref()
.map(|config| config.remote != remote || config.merge != remote_branch)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Idempotency check compares against wrong value.

Line 432 constructs merge_ref = format!("refs/heads/{remote_branch}"), and line 417 correctly writes merge_ref to the DB. However, the idempotency check here compares config.merge != remote_branch (the short name) instead of config.merge != merge_ref (the full ref path that was actually stored).

Since the stored value is refs/heads/main but remote_branch is just main, this comparison always evaluates to true, meaning the write is never skipped — the idempotency optimization is dead code.

Suggested fix:

.map(|config| config.remote != remote || config.merge != merge_ref)

Generated by Claude Code


// delete old branch
Branch::delete_branch(&old_name, None).await;
Branch::delete_branch_result(&old_name, None)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (pre-existing, low priority): Non-atomic renameBranch::update_branch creates the new branch (line 617), then Head::update moves HEAD (line 625), then Branch::delete_branch_result deletes the old branch (line 630). If the delete fails, both branches exist with no rollback. Consider wrapping these operations in a transaction via _with_conn variants to make the rename atomic, or at minimum document this as a known limitation.


Generated by Claude Code

.await;
) -> Result<(), BranchError> {
let (remote, remote_branch) = upstream
.split_once('/')
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge case: split_once('/') only splits on the first slash. For a branch like origin/feature/foo, this yields remote="origin" and remote_branch="feature/foo", which is correct. However, if someone has a remote named my/remote (unusual but valid in git), this would mis-parse. This matches git's own behavior, so it's fine for now — just flagging for awareness.


Generated by Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b48a0947ed

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +345 to +346
if let Err(error) = Self::delete_branch_result_with_conn(db, branch_name, remote).await {
log_branch_store_error(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate remote-branch delete failures to callers

delete_branch_with_conn now swallows all deletion errors and only logs via tracing, but tracing is initialized only when LIBRA_LOG/RUST_LOG is set (see src/main.rs), so default CLI runs drop the error entirely. Callers like remote prune (src/command/remote.rs) still print *[pruned]* and increment counts even if the DB delete failed, which creates a silent false-success path and leaves stale remote-tracking refs. Return a Result here (or otherwise surface a hard error) so commands cannot report successful pruning when nothing was deleted.

Useful? React with 👍 / 👎.

Signed-off-by: Quanyi Ma <eli@patch.sh>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 6 comments.

Comment on lines 369 to +373
// avoids holding the transaction open while doing read-only preparations.
let db = get_db_conn_instance().await;
let old_oid = Head::current_commit_with_conn(&db)
.await
.ok_or_else(|| "Cannot reset: HEAD is unborn and points to no commit.".to_string())?;
.ok_or(ResetError::HeadUnborn)?;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perform_reset() uses Head::current_commit_with_conn(&db) (lossy) and maps None directly to ResetError::HeadUnborn. But current_commit_with_conn now swallows DB/branch-store errors (logs and returns None), so a corrupted/failed HEAD resolution can be misreported as an unborn HEAD (RepoStateInvalid) instead of RepoCorrupt/IoReadFailed. Use Head::current_commit_result_with_conn(&db).await here and map Err(BranchStoreError::Query(_)) to an I/O read error, and other errors to repo corruption, while reserving Ok(None) for the true unborn-HEAD case.

Copilot uses AI. Check for mistakes.
Comment on lines 478 to 491
@@ -277,259 +487,363 @@ pub async fn create_branch_safe(
Head::Branch(name) => name,
Head::Detached(commit_hash) => commit_hash.to_string(),
};
return Err(
CliError::fatal(format!("not a valid object name: '{}'", current))
.with_stable_code(StableErrorCode::CliInvalidTarget),
);
return Err(BranchError::InvalidCommit(current));
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_branch_impl falls back to Head::current_commit().await (lossy) when no base is provided. Since Head::current_commit() now logs and returns None on branch-store failures, this path can turn repository corruption/DB read failures into a misleading InvalidCommit (CliInvalidTarget) error. Prefer Head::current_commit_result() so DB/query/corruption errors are surfaced with the right stable code, and reserve Ok(None) for the true unborn-HEAD case (likely RepoStateInvalid).

Copilot uses AI. Check for mistakes.
Signed-off-by: Quanyi Ma <eli@patch.sh>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.

Signed-off-by: Quanyi Ma <eli@patch.sh>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 3 comments.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a5667bac9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@genedna
Copy link
Copy Markdown
Member Author

genedna commented Apr 2, 2026

@codex review

Copy link
Copy Markdown
Member Author

@genedna genedna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review — PR #334 (v2, 4 new commits)

Previous Review Issues — Resolution Status

Issue Status Notes
Idempotency bug in set_upstream_with_conn (config.merge != remote_branch vs merge_ref) Fixed Comparison is now against the short name, with a comment explaining that branch_config_with_conn() normalizes refs/heads/<name><name>. Correct.
Duplicated short_hash/abbreviate_hash Fixed Consolidated into utils::text::short_display_hash with Unicode-safe char_indices() slicing and unit tests. Both local copies removed from branch.rs and tag.rs.
Silent unwrap_or_default() on get_commit_summary in reset.rs Fixed Replaced with load_commit_summary_or_warn() that logs at tracing::warn! before falling back.
Rollback warnings silently dropped in reset.rs Fixed Now logged via tracing::warn! with structured fields before discarding.
Non-atomic rename_branch_impl ⚠️ Not addressed Still three separate operations without a transaction wrapper. Acceptable as a known limitation for now.

All high-priority items are resolved. Nice work on the turnaround.


New Changes in v2 — Scope Expansion

The v2 update goes well beyond fixing the flagged issues. It systematically migrates 16 command files from silent-failure Branch::find_branch / Head::current_commit / Branch::list_branches (returning Option, swallowing DB errors) to new _result-suffixed variants that return Result. This is a significant robustness improvement across the codebase.

Files touched beyond original PR scope: switch.rs, status.rs, show.rs, show_ref.rs, log.rs, checkout.rs, clone.rs, push.rs, remote.rs, restore.rs, shortlog.rs, fetch.rs, merge.rs, cloud.rs, config.rs, local_client.rs


Code Quality

Strengths:

  • Consistent error-mapping pattern: each command defines a local map_*_branch_store_error helper that converts BranchStoreError into the command's own error type. This keeps concerns well-separated.
  • internal/head.rs adds current_commit_result / current_commit_result_with_conn following the established _result convention, with the old non-_result version now delegating and logging errors.
  • utils::text module is clean, well-tested, and properly exported.
  • The fetch.rs fix removing unwrap_or_default() on ConfigKv::all_remote_configs() closes a real silent-failure bug (the --all flag would silently skip all remotes if config reading failed).

Behavior changes to note:

  • Several commands now abort on DB read failures that were previously silently degraded (e.g., log.rs ref decorations, status.rs upstream info). This is the correct trade-off (surface errors rather than hide them), but it changes user-visible behavior in degraded environments.

Security

No new concerns. All DB operations continue to use Sea-ORM's parameterized queries. No user input reaches raw SQL.


Performance

  • get_commit_base in utils/util.rs now does a two-step remote branch resolution (full refs/remotes/origin/main first, then short-name fallback). This adds one extra DB query in the fallback path but correctly handles both fetched remote-tracking refs and directly-stored remote branches.
  • No other performance regressions identified.

Testing

New tests (strong coverage):

  • test_branch_set_upstream_idempotent_path_skips_redundant_write — proves the fixed idempotency check works by making the DB read-only after the first write. Clever technique.
  • test_branch_create_without_base_surfaces_corrupt_head_storage + two siblings — corrupt-ref fault injection tests for create, delete, and show-current paths.
  • test_branch_list_surfaces_corrupt_reference_name — tests list with a corrupt non-HEAD branch.
  • test_remote_prune_does_not_report_success_when_delete_fails — end-to-end prune test with permission manipulation.
  • test_reset_corrupt_head_reference_returns_repo_corrupt — with a negative regression guard against "HEAD is unborn" misclassification.
  • tests/local_client_test.rs — verifies cwd restoration after discovery_reference error.

Suggestions for additional coverage:

  • rename_branch_impl has no integration test (create → rename → verify HEAD update).
  • --show-current corrupt-HEAD test may be over-broad — show-current arguably should only read the branch name, not resolve the commit hash. Worth verifying this is intentional behavior rather than encoding a bug.

Summary of Remaining Items

Priority File Issue
Medium local_client.rs:73 with_repo_current_dir is not cancellation-safe — if the async future is dropped, cwd is not restored. Consider a Drop-based guard.
Low branch.rs Non-atomic rename still unaddressed (known limitation).
Low Test coverage No test for rename_branch_impl; --show-current corrupt-HEAD behavior should be verified as intentional.

Overall: This is a substantial quality improvement. The systematic migration to fallible APIs is well-executed and consistent. The previous review's high-priority items are all resolved. The remaining items are low-to-medium severity. LGTM with the local_client.rs cancellation concern as the only notable open item.


Generated by Claude Code

}

impl LocalClient {
async fn with_repo_current_dir<T, E, F, Fut>(&self, operation: F) -> Result<T, E>
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancellation safety concern: with_repo_current_dir changes process-global state (env::set_current_dir) without a Drop-based guard. If the operation().await future is cancelled (e.g., Tokio task abort, select! branch), the working directory is left pointing to self.repo_path with no restore.

Consider using a RAII guard struct with a Drop impl that restores the directory, similar to ChangeDirGuard in utils/test.rs. This would ensure restoration even on cancellation:

struct CwdGuard { original: PathBuf }
impl Drop for CwdGuard {
    fn drop(&mut self) {
        let _ = env::set_current_dir(&self.original);
    }
}

Alternatively, if cancellation is not expected in practice for these call sites, a brief // SAFETY: comment documenting this assumption would suffice.


Generated by Claude Code

&& let Some(branch) = Branch::find_branch(branch_name, Some(remote)).await
{
return Ok(branch.commit);
let remote_tracking_ref = format!("refs/remotes/{remote}/{branch_name}");
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvement: the two-step remote branch resolution (first try full refs/remotes/origin/main ref, then fall back to short main with remote=origin) correctly handles both fetched remote-tracking refs and directly-stored remote branches. The comment documenting this dual-lookup strategy is clear.


Generated by Claude Code

));
return Err(ResetError::PathspecWithHard);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review issue resolved: the silent unwrap_or_default() has been replaced with load_commit_summary_or_warn() which logs at tracing::warn! before falling back. This is exactly what was suggested. The rollback warning suppression (previously let _ = ...) is also now addressed with tracing::warn! logging. Both fixes look good.


Generated by Claude Code

pub const SHORT_HASH_LEN: usize = 7;

/// Return a shortened display form of a hash-like string without assuming ASCII.
pub fn short_display_hash(hash: &str) -> &str {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done: short_display_hash uses char_indices().nth() to find a safe byte boundary instead of raw byte slicing. This is strictly safer than the removed &hash[..end] pattern, even though git hashes are hex-only in practice. The unit test for UTF-8 boundaries (short_display_hash_respects_utf8_boundaries) validates this. Clean extraction from the duplicate implementations.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cedf0910b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/utils/util.rs:475

  • get_commit_base() now prefixes internal errors with "fatal: ..." (e.g. "fatal: failed to resolve HEAD", "fatal: HEAD does not point to a commit"). Several callers already wrap this error in their own user-facing message (e.g. CliError::fatal(format!("Cannot resolve ...: {e}"))), so this can lead to awkward/duplicated wording like "Cannot resolve 'X': fatal: ...". Consider returning plain, non-prefixed messages from this utility and letting the command layer decide whether/how to add fatal: / stable codes / hints.
pub async fn get_commit_base(name: &str) -> Result<ObjectHash, String> {
    // 1. Check for HEAD
    if name.to_uppercase() == "HEAD" {
        if let Some(commit_id) = Head::current_commit_result()
            .await
            .map_err(|error| format!("fatal: failed to resolve HEAD: {error}"))?
        {
            return Ok(commit_id);
        } else {
            return Err("fatal: HEAD does not point to a commit".to_string());
        }

let tag_list = tag::list().await.map_err(|e| e.to_string())?;
let tag_list = tag::list()
.await
.map_err(|e| CliError::failure(e.to_string()))?;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect_show_ref_entries() maps tag::list() failures to CliError::failure(e.to_string()) without an explicit StableErrorCode or context. This relies on message-based inference and can produce inconsistent error codes compared to the explicit branch/HEAD paths above. Consider wrapping with a clear context message (e.g. "failed to list tags") and setting an explicit stable code (likely RepoCorrupt or IoReadFailed, depending on how internal::tag::list() fails).

Suggested change
.map_err(|e| CliError::failure(e.to_string()))?;
.map_err(|e| {
CliError::failure(format!("failed to list tags: {e}"))
.with_stable_code(StableErrorCode::IoReadFailed)
})?;

Copilot uses AI. Check for mistakes.
@genedna
Copy link
Copy Markdown
Member Author

genedna commented Apr 2, 2026

@codex review

Copy link
Copy Markdown
Member Author

@genedna genedna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review — PR #334 (v3, 1 new commit)

Previous Review Issues — Resolution Status

Issue Status
with_repo_current_dir not cancellation-safe (no Drop-based guard) FixedRepoCurrentDirGuard with RAII Drop impl + explicit restore()
Non-atomic rename_branch_impl ⚠️ Known limitation (unchanged, acceptable)

Changes in v3

1. RepoCurrentDirGuard RAII guard (local_client.rs:57-97)

Exactly addresses the cancellation safety concern from the v2 review:

  • change_to() saves the original directory and switches to the repo path
  • restore() explicitly restores on the success path, setting restored = true
  • Drop restores the directory if restore() was never called (cancellation/panic), with tracing::error! on failure
  • with_repo_current_dir now uses the guard, with clean error handling: restore failure on success path becomes the error; restore failure on error path is logged and the original error is preserved

The cancellation test is well-crafted — uses oneshot to confirm the operation entered the changed directory, pending::<()>().await to block forever, then handle.abort() + handle.await to trigger Drop and verify cwd restoration.

2. map_local_branch_state_error in clone.rs

Replaces the previous blanket error.to_string()RepoCorrupt mapping with an exhaustive match on all four BranchStoreError variants. Notably, NotFound now correctly maps to RepoStateInvalid (not RepoCorrupt), which was flagged as a semantic imprecision in reset.rs during the v2 review. The clone.rs implementation sets the right precedent. Two unit tests cover the mapping.

3. branch -a unborn HEAD fix (branch.rs)

Fixes an edge case where branch -a on an unborn repo with remote-tracking refs would suppress the * main marker. The root cause was show_unborn_head checking entries.is_empty() (which includes remote entries) instead of local_branches_empty. The rendering order was also corrected. A new test validates this specific scenario.

Summary

All items from the v2 review are now addressed. The RepoCurrentDirGuard is the standout addition — it’s a textbook RAII implementation with proper cancellation safety and an excellent test. The exhaustive BranchStoreError matching in clone.rs and the unborn-HEAD edge case fix round out a clean revision.

No new issues found. LGTM.


Generated by Claude Code

/// This supports an explicit `restore()` so callers can surface restore
/// failures on the success path, while `Drop` still restores the directory if
/// the surrounding future is cancelled or aborted.
struct RepoCurrentDirGuard {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly what was suggested in the previous review. The Drop-based guard ensures cwd restoration even on async cancellation, while the explicit restore() method lets callers surface I/O failures on the success path. The restored flag avoids a redundant set_current_dir in the normal flow. Clean implementation.

The cancellation test (with_repo_current_dir_restores_current_dir_when_task_is_cancelled) is well-designed — using oneshot to confirm the operation entered the changed directory, then handle.abort() + handle.await to ensure the Drop has fully run before asserting. This directly validates the RAII guarantee.


Generated by Claude Code

}
}

fn map_local_branch_state_error(source: branch::BranchStoreError) -> CliError {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good: The previous review noted that reset.rs mapped BranchStoreError::NotFound to HeadCorrupt via a catch-all arm. Here in clone.rs, all four variants (Query, Corrupt, NotFound, Delete) are explicitly matched with semantically appropriate stable codes (IoReadFailed, RepoCorrupt, RepoStateInvalid, IoWriteFailed). This is the right pattern — NotFound correctly maps to RepoStateInvalid rather than RepoCorrupt. Well done.


Generated by Claude Code

});
}

let show_unborn_head = local_branches_empty
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix: The old code checked entries.is_empty() which included both local and remote branches. With branch -a on an unborn repo that has remote-tracking refs, entries would be non-empty (remote refs present), so the unborn HEAD marker * main was suppressed. Now local_branches_empty correctly decouples the unborn-HEAD display from the presence of remote entries.

The rendering change (moving the unborn-head print before the if branches.is_empty() { return Ok(()); } guard) ensures the unborn HEAD is shown even when remote branches follow. The new test test_branch_all_shows_unborn_head_even_with_remote_refs validates this precisely.


Generated by Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f783c2d643

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Signed-off-by: Quanyi Ma <eli@patch.sh>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d73bea0f24

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +542 to +549
other => {
tracing::error!(
"rollback after reset failed: {} (primary error: {})",
rollback_error,
other
);
other
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve rollback failure context for all reset errors

merge_reset_failure now drops rollback errors for primary variants outside IndexSave/HeadUpdate/WorktreeRead/WorktreeRestore and only emits a tracing::error!. In normal CLI runs (without verbose tracing), a failed rollback after a partial reset (for example, an ObjectLoad failure during hard reset) is hidden, so users get only the primary error and no indication that repository recovery also failed. This can leave index/worktree state partially mutated without actionable diagnostics; include rollback context in the returned error for all variants (or wrap both errors in a dedicated composite error).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

@genedna genedna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review — PR #334 (v4, 1 new commit)

Changes in v4

A focused update addressing CLI UX and show-ref --tags error classification.

1. Tag argument validation hoisted before repo check (cli.rs, tag.rs)

validate_cli_args is now called in parse_async before the repository-existence check. This ensures that libra tag -d (missing tag name) outside a repo gives a clear usage error rather than "not a Libra repository". The validation order in run_tag is also swapped (validate_named_tag_action before require_repo) for consistency.

The test test_tag_missing_name_usage_outranks_repo_not_found_outside_repo validates this with a negative assertion — good defensive testing.

2. show_ref_tag_list_error with DbErr downcast (show_ref.rs)

Since tag::list() returns an untyped anyhow::Error, the new error handler walks the error chain looking for DbErr to distinguish I/O failures (IoReadFailed) from object corruption (RepoCorrupt). This is a pragmatic workaround — see inline comment for a minor suggestion.

The test test_show_ref_surfaces_corrupt_tag_storage validates this by injecting a corrupt tag reference and asserting LBR-REPO-002.

Code Quality

  • Clean, focused changes with no unnecessary scope expansion.
  • Both new tests are well-structured with proper assertions on exit code, error code, and message content.
  • The dual validation (once in cli.rs, once in run_tag) is harmless and keeps run_tag self-contained.

No New Issues Found

This is a clean polish pass. LGTM.


Generated by Claude Code

}
}

fn show_ref_tag_list_error(error: anyhow::Error) -> CliError {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting approach: using error.chain().any(|cause| cause.downcast_ref::<DbErr>().is_some()) to distinguish DB I/O failures from object-level corruption when tag::list() returns an untyped anyhow::Error. This is a pragmatic workaround given that tag::list() doesn't yet return a typed error enum.

One consideration: if tag::list() is ever refactored to return a typed error (like TagStoreError), this downcast heuristic becomes unnecessary. A brief // TODO: comment noting this is a workaround for untyped tag::list() errors would help future readers. Low priority — not blocking.


Generated by Claude Code

_ => return Err(classify_parse_error(&argv, &err)),
},
};
if let Commands::Tag(tag_args) = &args.command {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good UX improvement: hoisting validate_cli_args before the repo-existence check ensures that libra tag -d outside a repository gives a clear usage error ("tag name is required for --delete") instead of the less helpful "not a Libra repository". The test test_tag_missing_name_usage_outranks_repo_not_found_outside_repo validates this ordering explicitly, including a negative assertion that the repo error does not appear.

The corresponding reorder in run_tag (validation before require_repo) is consistent — though now the validation runs twice (once in cli.rs, once in run_tag). The duplication is harmless (it's a pure check with no side effects) and keeps run_tag self-contained for callers that bypass parse_async.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 3 comments.

Comment on lines +129 to +137
Err(error) => {
tracing::error!(
repo_path = %self.repo_path.display(),
restore_dir = %guard.original_dir.display(),
error = %restore_error,
"failed to restore working directory after local protocol operation"
);
Err(error)
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_repo_current_dir() logs when restore() fails and operation() already returned an error, but RepoCurrentDirGuard will also try to restore again in Drop and log on failure. In the common case where restore keeps failing, this produces duplicate error logs for a single operation. Consider logging in only one place (e.g., remove the explicit log here and rely on Drop, or add a restore_attempted flag so Drop can retry without re-logging).

Suggested change
Err(error) => {
tracing::error!(
repo_path = %self.repo_path.display(),
restore_dir = %guard.original_dir.display(),
error = %restore_error,
"failed to restore working directory after local protocol operation"
);
Err(error)
}
Err(error) => Err(error),

Copilot uses AI. Check for mistakes.
Comment on lines +223 to +226
tag::CreateTagError::CheckExisting(source) => TagError::CheckExistingFailed {
name: tag_name.to_string(),
detail: source.to_string(),
},
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateTagError::CheckExisting carries a DbErr, but map_create_tag_error() stringifies it into TagError::CheckExistingFailed { detail: String }, which discards the underlying error type/chain. That makes it hard to map query failures to the correct StableErrorCode (e.g., IoReadFailed vs RepoCorrupt) and prevents downstream helpers (like show-ref’s DbErr detection) from working. Consider storing the source error (e.g., DbErr/anyhow::Error) in TagError and mapping stable codes based on the concrete cause.

Copilot uses AI. Check for mistakes.
Comment on lines +345 to +348
async fn collect_tags(show_lines: usize) -> Result<Vec<TagListEntry>, TagError> {
let tags = tag::list()
.await
.map_err(|e| TagError::ListFailed(e.to_string()))?;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect_tags() converts the anyhow::Error from internal::tag::list() into TagError::ListFailed(String) via to_string(), losing the error chain (notably DbErr). This prevents accurate stable-code classification (I/O read vs repo corrupt) and makes debugging harder. Prefer keeping the original error as a source (e.g., ListFailed(#[source] anyhow::Error)), or classify into IoReadFailed/RepoCorrupt here before stringifying.

Copilot uses AI. Check for mistakes.
Signed-off-by: Eli Ma <eli@patch.sh>
@genedna
Copy link
Copy Markdown
Member Author

genedna commented Apr 2, 2026

@codex review

Copy link
Copy Markdown
Member Author

@genedna genedna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review — PR #334 (v5, 1 new commit)

Changes in v5

Three focused improvements addressing error context preservation and error classification accuracy.

1. ResetError::Rollback variant (reset.rs)

Replaces the previous merge_reset_failure approach (string-concatenation into specific variants, with tracing::error! swallowing for non-appendable variants) with a structured Rollback { primary: Box<ResetError>, rollback: Box<ResetError> } variant. This is a significant improvement:

  • All error combinations are now preserved — no silent swallowing
  • stable_code() / hint() / is_command_usage() delegate to primary, so exit codes and hints are always correct
  • Display format "{primary}; rollback failed: {rollback}" gives full diagnostic context

The From<ResetError> for CliError is also refactored from a large per-variant match into a clean delegation through stable_code(), hint(), and is_command_usage() methods. The updated test asserts both the stable code and that the message contains both the primary and rollback error text.

2. TagError typed #[source] annotations (tag.rs)

Replaces detail: String fields with typed #[source] fields (DbErr, GitError, io::Error, anyhow::Error). This preserves the original error chain and enables the classify_tag_read_error function to use DbErr downcasting for precise IoReadFailed vs RepoCorrupt classification. Also fixes CheckExistingFailed from RepoCorrupt to IoReadFailed — correct reclassification since a DB query failure is I/O, not corruption. Three new unit tests validate the classification.

3. RepoCurrentDirGuard duplicate-logging prevention (local_client.rs)

Adds restore_failure_logged flag to prevent Drop from re-logging a restore failure that with_repo_current_dir already logged on the error path. Small but thoughtful refinement.

4. show_ref.rs TODO comment — Added per v4 review suggestion, noting the DbErr-chain heuristic is a workaround pending a typed tag::list() error.

Code Quality

  • The ResetError impl methods (stable_code, hint, is_command_usage) are clean and exhaustive. The Rollback variant delegates correctly in all three.
  • The TagError source type changes are backwards-compatible — map_create_tag_error now passes through the original source directly instead of .to_string().
  • render_tags now uses anyhow::Error::from instead of anyhow::anyhow!(e.to_string()) for proper error chain preservation.

No New Issues Found

This is a quality-focused polish pass that improves error fidelity throughout. LGTM.


Generated by Claude Code

#[error("pathspec '{0}' did not match any file(s) known to libra")]
PathspecNotMatched(String),

#[error("{primary}; rollback failed: {rollback}")]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent refactor. The Rollback variant with Box<ResetError> for both primary and rollback errors replaces the previous merge_reset_failure which string-concatenated into specific variants or silently logged via tracing::error! for non-appendable ones. Now:

  1. All error combinations are preserved — no more tracing::error! swallowing the rollback error for ObjectLoad or other variants.
  2. stable_code() / hint() / is_command_usage() all delegate to primary, so the user sees the root cause's exit code and hint.
  3. The Display format "{primary}; rollback failed: {rollback}" gives full context in a single error message.

The refactored From<ResetError> for CliError is also much cleaner — stable_code(), hint(), and is_command_usage() methods eliminate the large match block with repetitive .with_stable_code() / .with_hint() chains.


Generated by Claude Code

#[error("Cannot create tag: HEAD does not point to a commit")]
HeadUnborn,

#[error("failed to read existing tags before creating '{name}': {source}")]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good improvement: replacing detail: String with typed #[source] fields (DbErr, GitError, io::Error, anyhow::Error) preserves the original error chain. This enables the classify_tag_read_error function to use error.chain().any(|cause| cause.downcast_ref::<DbErr>().is_some()) for precise error classification.

Also note the CheckExistingFailed stable code fix from RepoCorrupt to IoReadFailed — a DB query failure during the existence check is an I/O issue, not corruption. Correct reclassification.


Generated by Claude Code

/// This supports an explicit `restore()` so callers can surface restore
/// failures on the success path, while `Drop` still restores the directory if
/// the surrounding future is cancelled or aborted.
struct RepoCurrentDirGuard {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice attention to detail: the restore_failure_logged flag prevents duplicate error logging when with_repo_current_dir already logged the restore failure on the error path and then Drop fires again. Without this, users would see the same tracing::error! message twice. Clean solution.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 37 changed files in this pull request and generated 3 comments.

Comment on lines +143 to +149
let repo = create_committed_repo_via_cli();
let db_path = repo.path().join(".libra").join("libra.db");
let original_mode = fs::metadata(&db_path).unwrap().permissions().mode();

fs::set_permissions(&db_path, std::fs::Permissions::from_mode(0o444)).unwrap();
let output = run_libra_command(&["branch", "--set-upstream-to", "origin/main"], repo.path());
fs::set_permissions(&db_path, std::fs::Permissions::from_mode(original_mode)).unwrap();
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test relies on chmod 0444 to force a SQLite write failure, but on self-hosted CI runners the test process may run as root (or with elevated capabilities), in which case writes can still succeed and the assertion becomes flaky. Consider skipping the permission-based assertion when effective UID==0, or use a deterministic failure injection approach (e.g., read-only DB connection / mocked persistence) so the test outcome is independent of runner privileges.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +174
let db_path = repo.path().join(".libra").join("libra.db");
let original_mode = fs::metadata(&db_path).unwrap().permissions().mode();

fs::set_permissions(&db_path, std::fs::Permissions::from_mode(0o444)).unwrap();
let second = run_libra_command(&["branch", "--set-upstream-to", "origin/main"], repo.path());
fs::set_permissions(&db_path, std::fs::Permissions::from_mode(original_mode)).unwrap();

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This idempotency test again depends on chmod 0444 to ensure a redundant config write would fail. If the runner executes tests as root, the write may still succeed and the test won’t be exercising the intended failure path, which can make CI results environment-dependent. Consider guarding the test under effective UID!=0 or refactoring to assert idempotency without filesystem permission assumptions.

Copilot uses AI. Check for mistakes.
Comment on lines +868 to +873
let db_path = repo_dir.join(".libra").join("libra.db");
let original_mode = fs::metadata(&db_path).unwrap().permissions().mode();
fs::set_permissions(&db_path, std::fs::Permissions::from_mode(0o444)).unwrap();
let output = run_libra_command(&["remote", "prune", "origin"], &repo_dir);
fs::set_permissions(&db_path, std::fs::Permissions::from_mode(original_mode)).unwrap();

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test forces an I/O error by chmod-ing the SQLite DB to 0444. If tests run as root on self-hosted CI, the prune may still be able to write/delete and the test becomes flaky or ineffective. Consider skipping when effective UID==0 or using a deterministic failure injection that doesn’t depend on Unix permission enforcement.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants