Skip to content

Conversation

elmattic
Copy link
Contributor

@elmattic elmattic commented Oct 3, 2025

Summary of changes

Changes introduced in this pull request:

  • Add forest-cli snasphot export-status
  • Add forest-cli snasphot export-cancel
$ forest-cli snapshot export-status --wait   
[00:01:37] [####################################################>----------------] 76% Exporting

Reference issue to close (if applicable)

Closes #6082

Other information and links

Change checklist

  • I have performed a self-review of my own code,
  • I have made corresponding changes to the documentation. All new code adheres to the team's documentation standards,
  • I have added tests that prove my fix is effective or that my feature works (if possible),
  • I have made sure the CHANGELOG is up-to-date. All user-facing changes should be reflected in this document.

Summary by CodeRabbit

  • New Features

    • Monitor snapshot export progress and cancel running exports via new RPC endpoints and CLI commands.
    • CLI: snapshot export-status (supports --wait with live progress and JSON/Text output) and snapshot export-cancel.
    • Exports now return structured outcomes and print "Export completed." or "Export cancelled.".
  • Documentation

    • Changelog updated with export status and cancel entries.
  • Tests

    • Snapshot-related test ignores updated to include new status/cancel endpoints.

Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

Walkthrough

Adds RPCs and CLI commands to monitor and cancel long-running chain snapshot exports, introduces ApiExportStatus/ApiExportResult types and global export state, instruments chain streaming for progress tracking, and makes one block header field public.

Changes

Cohort / File(s) Summary of changes
Changelog
CHANGELOG.md
Added unreleased entry documenting new CLI snapshot subcommands export-status and export-cancel.
CLI: snapshot commands
src/cli/subcommands/snapshot_cmd.rs
Added public Format enum; added SnapshotCommands variants ExportStatus { wait: bool, format: Format } and ExportCancel {}; adjusted export flow to handle ApiExportResult (Done/Cancelled); implemented status polling with --wait, progress UI, and explicit cancel command.
RPC: types
src/rpc/types/mod.rs
Added ApiExportStatus (progress, exporting, cancelled, start_time) and ApiExportResult enum (Done(Option<String>), Cancelled) with serde/JsonSchema and Lotus JSON registration.
RPC: methods & registration
src/rpc/methods/chain.rs, src/rpc/mod.rs
ForestChainExport/ChainExport now return ApiExportResult; added ForestChainExportStatus and ForestChainExportCancel RPCs; handlers use a cancellation token (CHAIN_EXPORT_LOCK), call start_export/end_export, and return structured results.
IPLD streaming & status
src/ipld/util.rs
Added public ExportStatus and CHAIN_EXPORT_STATUS (LazyLock<Mutex<...>>); lifecycle functions start_export/end_export/cancel_export; added track_progress builder flag on ChainStream and updated traversal to update export epoch when tracking enabled.
Chain export orchestration
src/chain/mod.rs
Enabled .track_progress(true) when building export streams to emit progress updates during export.
Blocks: header visibility
src/blocks/header.rs
Made CachingBlockHeader.uncached public (pub uncached: RawBlockHeader).
Tests / tooling
src/tool/subcommands/api_cmd/test_snapshots_ignored.txt
Added Forest.ChainExportStatus and Forest.ChainExportCancel to ignored snapshot tests list.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant CLI
    participant RPC as "RPC Server"
    participant Export as "Export Task"
    participant Status as "CHAIN_EXPORT_STATUS"
    participant Cancel as "CANCELLATION_TOKEN"

    CLI->>RPC: ForestChainExport(params)
    RPC->>Status: start_export()
    RPC->>Export: start export_to_forest_car(track_progress=true)
    par Export completes or Cancel signalled
        Export-->>RPC: Result (hash / error)
        Cancel-->>RPC: cancellation signal
    end
    RPC->>Status: end_export() / mark cancelled
    alt Completed
        RPC-->>CLI: ApiExportResult::Done(Optional<Hash>)
    else Cancelled
        RPC-->>CLI: ApiExportResult::Cancelled
    end
Loading
sequenceDiagram
    autonumber
    participant CLI
    participant RPC as "RPC Server"
    participant Status as "CHAIN_EXPORT_STATUS"

    CLI->>RPC: ForestChainExportStatus (--wait?)
    alt --wait
        loop while exporting
            RPC-->>CLI: ApiExportStatus { progress, exporting, cancelled, start_time }
        end
    else not waiting
        RPC-->>CLI: ApiExportStatus
    end
    CLI->>RPC: ForestChainExportCancel
    RPC->>Status: request cancellation (notify token)
    RPC-->>CLI: bool (cancellation requested)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

RPC

Suggested reviewers

  • hanabi1224
  • akaladarshi

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning Aside from snapshot export features, the change to make the uncached field in CachingBlockHeader public appears unrelated to the objectives of issue #6082 and isn’t referenced by the new export commands. Please validate whether the visibility change to uncached in src/blocks/header.rs is required for the snapshot export functionality and remove or separate it if it is out of scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely describes the primary change of adding new CLI subcommands for snapshot operations in forest-cli, aligning with the PR’s content despite a minor typo.
Linked Issues Check ✅ Passed The pull request implements all core objectives of issue #6082 by exposing export progress via new RPC and CLI subcommands, supporting a --wait flag for live progress display, and enabling cancellation of in-flight exports with confirmation, aligning with the documented behaviors.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch elmattic/snapshot-export-qol

Comment @coderabbitai help to get the list of available commands and usage tips.

@elmattic elmattic marked this pull request as ready for review October 8, 2025 13:56
@elmattic elmattic requested a review from a team as a code owner October 8, 2025 13:56
@elmattic elmattic requested review from akaladarshi and hanabi1224 and removed request for a team October 8, 2025 13:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (7)
src/rpc/types/mod.rs (1)

33-33: Add safety notes for status fields; otherwise good

  • Chrono DateTime with serde is fine; lotus_json wrapper will pass through.
  • Consider adding #[serde(rename_all = "PascalCase")] for consistency with adjacent types (optional).

Also applies to: 566-582

src/chain/mod.rs (1)

31-36: Cancellation notifier risks missed or stale signals

Using a global Notify without a per-job “generation” can race:

  • If cancel is called before the export starts awaiting, notify_waiters() does nothing and the cancel is lost (race).
  • If switched to notify_one() to avoid loss, a late cancel may leave a pending permit and cancel the next export spuriously.

Prefer a per-export token:

  • Use tokio_util::sync::CancellationToken stored for the active export (e.g., in a Mutex<Option>). On start, create/replace the token; in cancel, call token.cancel(); in the export, select on token.cancelled().
src/cli/subcommands/snapshot_cmd.rs (3)

142-147: Fix progress template: use current bytes, not total.

You're updating position but not setting a length, so {binary_total_bytes} won’t show progress. Use {binary_bytes}.

-    ProgressStyle::with_template(
-        "{spinner} {msg} {binary_total_bytes} written in {elapsed} ({binary_bytes_per_sec})",
-    )
+    ProgressStyle::with_template(
+        "{spinner} {msg} {binary_bytes} written in {elapsed} ({binary_bytes_per_sec})",
+    )

212-237: Throttle polling; cleanly clear the bar on early exit.

Polling every 10ms will hammer the RPC. Use ~500–1000ms. Also abandon/finish the bar when returning early to avoid a stale line.

-                        if !result.exporting {
-                            return Ok(());
-                        }
+                        if !result.exporting {
+                            pb.abandon();
+                            return Ok(());
+                        }
@@
-                        tokio::time::sleep(Duration::from_millis(10)).await;
+                        tokio::time::sleep(Duration::from_millis(750)).await;

253-255: Improve UX on no-op cancel.

Print a message when there’s nothing to cancel.

-                if result {
-                    println!("Export cancelled.");
-                }
+                if result {
+                    println!("Export cancelled.");
+                } else {
+                    println!("No export in progress.");
+                }
src/ipld/util.rs (2)

32-34: Prefer explicit Mutex::new for clarity.

Avoid relying on Into for Mutex construction; it’s less idiomatic and may surprise readers.

-pub static CHAIN_EXPORT_STATUS: LazyLock<Mutex<ExportStatus>> =
-    LazyLock::new(|| ExportStatus::default().into());
+pub static CHAIN_EXPORT_STATUS: LazyLock<Mutex<ExportStatus>> =
+    LazyLock::new(|| Mutex::new(ExportStatus::default()));

298-300: Use consistent epoch access.

Elsewhere this module and callers use block.epoch. Mixing block.uncached.epoch bypasses invariants and is inconsistent.

-                            update_epoch(block.uncached.epoch);
+                            update_epoch(block.epoch);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2bafb16 and c837c8a.

📒 Files selected for processing (8)
  • CHANGELOG.md (1 hunks)
  • src/blocks/header.rs (1 hunks)
  • src/chain/mod.rs (2 hunks)
  • src/cli/subcommands/snapshot_cmd.rs (3 hunks)
  • src/ipld/util.rs (6 hunks)
  • src/rpc/methods/chain.rs (6 hunks)
  • src/rpc/mod.rs (1 hunks)
  • src/rpc/types/mod.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/rpc/methods/chain.rs (2)
src/ipld/util.rs (3)
  • cancel_export (57-61)
  • end_export (52-55)
  • start_export (43-50)
src/chain/mod.rs (3)
  • export (52-52)
  • export (58-67)
  • export_v2 (71-133)
src/ipld/util.rs (2)
src/blocks/header.rs (3)
  • cid (69-71)
  • cid (304-306)
  • new (282-288)
src/blocks/tipset.rs (5)
  • cid (40-48)
  • new (242-260)
  • new (494-511)
  • epoch (306-308)
  • epoch (543-545)
src/cli/subcommands/snapshot_cmd.rs (1)
src/rpc/methods/chain.rs (18)
  • chain (375-375)
  • chain (413-413)
  • handle (145-147)
  • handle (161-173)
  • handle (187-202)
  • handle (217-231)
  • handle (246-278)
  • handle (291-299)
  • handle (312-323)
  • handle (336-439)
  • handle (452-465)
  • handle (478-487)
  • handle (500-543)
  • handle (556-580)
  • handle (596-605)
  • handle (620-625)
  • handle (640-678)
  • handle (693-709)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: tests-release
  • GitHub Check: tests
  • GitHub Check: Build Ubuntu
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build MacOS
  • GitHub Check: All lint checks
🔇 Additional comments (5)
src/blocks/header.rs (1)

239-239: Clarify necessity of public uncached field (src/blocks/header.rs:239)

  • Currently only used internally (e.g., src/ipld/util.rs:299); if it’s not consumed by external crates, restrict visibility to pub(crate) or expose via a getter.
  • Fully public access allows direct mutation, risking cached CID/signature state becoming inconsistent.
  • Please document the snapshot-export use case that requires this field to be fully public.
CHANGELOG.md (1)

38-39: Changelog entry looks good

Entry correctly reflects the new CLI subcommands under “Added”.

src/rpc/mod.rs (1)

86-88: Method registration looks correct

Both ForestChainExportStatus and ForestChainExportCancel are properly registered under the chain vertical.

src/chain/mod.rs (1)

167-169: Progress tracking enablement

Enabling track_progress(true) here is appropriate to surface export status. Ensure the tracker correctly bounds values [0,1] to avoid NaN/Inf in RPC (see related comment in chain RPC).

src/cli/subcommands/snapshot_cmd.rs (1)

119-123: No changes required: DateTime::from_timestamp(secs, nanos) is provided by chrono 0.4.x.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/cli/subcommands/snapshot_cmd.rs (3)

164-166: Previous concern remains: Duration::MAX in timeout.

As flagged in the previous review, Duration::MAX can overflow or misbehave in timer math. Consider using a large finite timeout.

Apply this diff to use a finite timeout:

-    let export_result = client
-        .call(ForestChainExport::request((params,))?.with_timeout(Duration::MAX))
-        .await?;
+    let export_result = client
+        .call(ForestChainExport::request((params,))?.with_timeout(Duration::from_secs(6 * 60 * 60)))
+        .await?;

172-178: Previous concern remains: Persist before checksum, avoid let-chains.

As flagged in the previous review:

  1. Persisting the file after writing the checksum can leave an orphaned checksum file if persist fails.
  2. The if-let chain syntax may pose MSRV concerns depending on the project's minimum Rust version.
  3. The clone() on line 172 is unnecessary since export_result is only used once more in the match statement below.

Apply this diff:

-    if !dry_run && let ApiExportResult::Done(hash_opt) = export_result.clone() {
-        if let Some(hash) = hash_opt {
-            save_checksum(&output_path, hash).await?;
-        }
-
-        temp_path.persist(output_path)?;
-    }
+    if !dry_run {
+        match &export_result {
+            ApiExportResult::Done(hash_opt) => {
+                // Persist first to avoid orphaned checksum on failure
+                temp_path.persist(&output_path)?;
+                if let Some(hash) = hash_opt {
+                    save_checksum(&output_path, hash.clone()).await?;
+                }
+            }
+            ApiExportResult::Cancelled => {
+                // No file to persist on cancel
+            }
+        }
+    }

239-244: Previous concern remains: Duration::MAX in timeout.

As flagged in the previous review, Duration::MAX can overflow or misbehave in timer math. For a simple status query, a shorter timeout is more appropriate.

Apply this diff:

-    let result = client
-        .call(ForestChainExportStatus::request(())?.with_timeout(Duration::MAX))
-        .await?;
+    let result = client
+        .call(ForestChainExportStatus::request(())?.with_timeout(Duration::from_secs(30)))
+        .await?;
🧹 Nitpick comments (1)
src/cli/subcommands/snapshot_cmd.rs (1)

246-256: Add user feedback when no export is running.

When result is false (no export is currently running), the command exits silently. Users would benefit from explicit feedback in this case.

Apply this diff:

     let result = client
         .call(
             ForestChainExportCancel::request(())?.with_timeout(Duration::from_secs(30)),
         )
         .await?;
-    if result {
+    if result {
         println!("Export cancelled.");
+    } else {
+        println!("No export is currently running.");
     }
     Ok(())
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c837c8a and 7760b22.

📒 Files selected for processing (1)
  • src/cli/subcommands/snapshot_cmd.rs (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/cli/subcommands/snapshot_cmd.rs (1)
src/rpc/methods/chain.rs (16)
  • handle (145-147)
  • handle (161-173)
  • handle (187-202)
  • handle (217-231)
  • handle (246-278)
  • handle (291-299)
  • handle (312-323)
  • handle (336-439)
  • handle (452-465)
  • handle (478-487)
  • handle (500-543)
  • handle (556-580)
  • handle (596-605)
  • handle (620-625)
  • handle (640-678)
  • handle (693-709)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: tests-release
  • GitHub Check: tests
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: Build MacOS
  • GitHub Check: Build Ubuntu
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: All lint checks
🔇 Additional comments (3)
src/cli/subcommands/snapshot_cmd.rs (3)

10-10: LGTM!

The new import is necessary to handle the export result variants (Done/Cancelled).


46-53: LGTM!

The new command variants are well-structured and align with the PR objectives.


180-188: LGTM!

The user feedback messages for completion and cancellation are appropriate.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7e2148 and 36faa47.

📒 Files selected for processing (1)
  • src/tool/subcommands/api_cmd/test_snapshots_ignored.txt (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: Script linters
src/tool/subcommands/api_cmd/test_snapshots_ignored.txt

[error] 1-1: git diff --exit-code reported differences in test_snapshots_ignored.txt; exit code 1. This indicates the snapshots are not sorted/added as expected by 'make sort-lists'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: tests
  • GitHub Check: tests-release
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build Ubuntu
  • GitHub Check: Build MacOS
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: All lint checks

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
src/cli/subcommands/snapshot_cmd.rs (4)

214-241: Add overall timeout to prevent indefinite polling.

The polling loop has no maximum duration, so if the server becomes unresponsive while an export is running, this will poll indefinitely.

Apply this diff:

+    let start = std::time::Instant::now();
+    let max_wait = Duration::from_secs(12 * 60 * 60); // 12 hours max
     loop {
+        if start.elapsed() > max_wait {
+            pb.abandon_with_message("Export timed out");
+            anyhow::bail!("Export status polling timed out after {} hours", max_wait.as_secs() / 3600);
+        }
         let result = client

175-191: Persist file before writing checksum; also avoid if-let chains.

The current order (checksum → persist) can orphan the checksum file if persist fails. Additionally, the if-let chain at line 175 may not compile on older Rust versions.

Apply this diff to reorder and restructure:

-                if !dry_run && let ApiExportResult::Done(hash_opt) = export_result.clone() {
-                    if let Some(hash) = hash_opt {
-                        save_checksum(&output_path, hash).await?;
-                    }
-
-                    temp_path.persist(output_path)?;
-                }
+                if !dry_run {
+                    match export_result.clone() {
+                        ApiExportResult::Done(hash_opt) => {
+                            // Persist first to prevent orphaned checksum on failure
+                            temp_path.persist(&output_path)?;
+                            if let Some(hash) = hash_opt {
+                                save_checksum(&output_path, hash).await?;
+                            }
+                        }
+                        ApiExportResult::Cancelled => { /* no file to persist */ }
+                    }
+                }

227-228: Add completion message when export finishes.

When !result.exporting becomes true (export finished), the code returns silently without updating the progress bar or informing the user.

Apply this diff:

         if !result.exporting {
+            pb.finish_with_message("Export completed");
             return Ok(());
         }

230-230: Clamp progress value defensively.

If the server returns an invalid progress value (e.g., negative or > 1.0), the cast could produce unexpected results.

Apply this diff:

-        let position = (result.progress * 10000.0).trunc() as u64;
+        let position = (result.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
🧹 Nitpick comments (3)
src/cli/subcommands/snapshot_cmd.rs (3)

236-236: Reduce polling frequency.

A 10ms sleep interval allows up to 100 polls per second, which is unnecessarily aggressive and wastes CPU/network resources.

Apply this diff to use a more reasonable interval:

-        tokio::time::sleep(Duration::from_millis(10)).await;
+        tokio::time::sleep(Duration::from_millis(500)).await;

247-247: Consider structured output format.

The Debug format ({:?}) isn't user-friendly or easily parsable. Consider supporting a --format json flag (as suggested by @hanabi1224) or using a custom Display implementation.

Example for custom Display:

println!("Export status:");
println!("  Progress: {:.1}%", result.progress * 100.0);
println!("  Exporting: {}", result.exporting);
println!("  Cancelled: {}", result.cancelled);
println!("  Started: {}", result.start_time);

257-259: Provide feedback when no export is in progress.

When result is false (no active export), the command prints nothing, leaving the user uncertain about what happened.

Apply this diff:

                 if result {
                     println!("Export cancelled.");
+                } else {
+                    println!("No export in progress.");
                 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9dab4a and 741918f.

📒 Files selected for processing (1)
  • src/cli/subcommands/snapshot_cmd.rs (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/cli/subcommands/snapshot_cmd.rs (1)
src/rpc/methods/chain.rs (16)
  • handle (145-169)
  • handle (212-224)
  • handle (238-253)
  • handle (268-282)
  • handle (297-329)
  • handle (342-350)
  • handle (363-374)
  • handle (387-490)
  • handle (503-516)
  • handle (529-538)
  • handle (551-594)
  • handle (607-631)
  • handle (647-656)
  • handle (671-676)
  • handle (691-729)
  • handle (744-760)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build MacOS
  • GitHub Check: Build Ubuntu
  • GitHub Check: tests
  • GitHub Check: tests-release
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: All lint checks
🔇 Additional comments (3)
src/cli/subcommands/snapshot_cmd.rs (3)

10-10: LGTM!

The import of ApiExportResult is necessary for handling the new export result variants (Done/Cancelled).


46-53: LGTM!

The new subcommand variants align with the PR objectives for exposing export status and cancellation controls.


164-169: LGTM!

The export RPC call correctly uses a 30-second timeout, which is appropriate since the export operation runs asynchronously on the server.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/cli/subcommands/snapshot_cmd.rs (3)

164-166: Address the Duration::MAX timeout concern.

The previous review comment about avoiding Duration::MAX remains unaddressed. Using Duration::MAX can overflow or misbehave in timer math.

Apply this diff to use a large finite timeout:

-                let export_result = client
-                    .call(ForestChainExport::request((params,))?.with_timeout(Duration::MAX))
-                    .await?;
+                let export_result = client
+                    .call(ForestChainExport::request((params,))?.with_timeout(Duration::from_secs(6 * 60 * 60)))
+                    .await?;

Also applies to lines 241-243.


172-178: Persist the file before writing the checksum.

The previous review comment about reordering persist and checksum operations remains unaddressed. If persist fails after writing the checksum, an orphaned checksum file will remain. Additionally, the if-let chain pattern may have MSRV implications.

Apply this diff to persist first, then write the checksum:

-                if !dry_run && let ApiExportResult::Done(hash_opt) = export_result.clone() {
-                    if let Some(hash) = hash_opt {
-                        save_checksum(&output_path, hash).await?;
-                    }
-
-                    temp_path.persist(output_path)?;
-                }
+                if !dry_run {
+                    match export_result.clone() {
+                        ApiExportResult::Done(hash_opt) => {
+                            temp_path.persist(&output_path)?;
+                            if let Some(hash) = hash_opt {
+                                save_checksum(&output_path, hash).await?;
+                            }
+                        }
+                        _ => {}
+                    }
+                }

190-238: Add completion message and defensive progress handling.

The previous review comment identified several concerns that remain partially unaddressed:

  1. Lines 224-225: When the export finishes normally (!result.exporting), no completion message is printed and the progress bar is not updated to 100%. Users won't see clear feedback that the export succeeded.

  2. Line 227: The progress calculation lacks defensive bounds checking. If the server returns an invalid progress value (e.g., negative or > 1.0), the cast could produce unexpected results.

  3. No overall timeout: The polling loop could run indefinitely if the server becomes unresponsive.

Apply this diff to address these concerns:

     pb.set_style(
         ProgressStyle::with_template(
             "[{elapsed_precise}] [{wide_bar}] {percent}% {msg} ",
         )
         .expect("indicatif template must be valid")
         .progress_chars("#>-"),
     );
+    let start = std::time::Instant::now();
+    let max_wait = Duration::from_secs(12 * 60 * 60); // 12 hours max
     loop {
+        if start.elapsed() > max_wait {
+            pb.abandon_with_message("Export timed out");
+            anyhow::bail!("Export status polling timed out after {} hours", max_wait.as_secs() / 3600);
+        }
         let result = client
             .call(
                 ForestChainExportStatus::request(())?
                     .with_timeout(Duration::from_secs(30)),
             )
             .await?;
         if result.cancelled {
             pb.set_message("Export cancelled");
             pb.abandon();
 
             return Ok(());
         }
         if !result.exporting {
+            pb.set_position(10000);
+            pb.finish_with_message("Export completed");
             return Ok(());
         }
-        let position = (result.progress * 10000.0).trunc() as u64;
+        let position = (result.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
         pb.set_position(position);
 
         if position == 10000 {
             break;
         }
         tokio::time::sleep(Duration::from_millis(10)).await;
     }
     pb.finish_with_message("Export completed");
 
     return Ok(());
🧹 Nitpick comments (2)
src/cli/subcommands/snapshot_cmd.rs (2)

239-246: Consider adding JSON format support.

A previous comment on line 50 suggested supporting --format json to facilitate bash scripting. This would allow users to programmatically parse the export status.

Based on learnings from past review comments.


248-258: Provide feedback when there's no export to cancel.

When result is false, the user receives no feedback. Consider printing a message to indicate that no export is currently in progress.

Apply this diff to improve user feedback:

                 if result {
                     println!("Export cancelled.");
+                } else {
+                    println!("No export in progress.");
                 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 741918f and ee3bb9c.

📒 Files selected for processing (1)
  • src/cli/subcommands/snapshot_cmd.rs (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/cli/subcommands/snapshot_cmd.rs (1)
src/rpc/methods/chain.rs (16)
  • handle (145-169)
  • handle (212-224)
  • handle (238-253)
  • handle (268-282)
  • handle (297-329)
  • handle (342-350)
  • handle (363-374)
  • handle (387-490)
  • handle (503-516)
  • handle (529-538)
  • handle (551-594)
  • handle (607-631)
  • handle (647-656)
  • handle (671-676)
  • handle (691-729)
  • handle (744-760)
🔇 Additional comments (1)
src/cli/subcommands/snapshot_cmd.rs (1)

46-53: LGTM!

The new subcommand variants align well with the PR objectives and linked issue requirements. The --wait flag for ExportStatus provides the desired polling behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/cli/subcommands/snapshot_cmd.rs (2)

164-166: Duration::MAX timeout issue previously flagged.

The use of Duration::MAX for the export timeout was already discussed in previous review comments. See the existing comment thread for details.


195-243: Multiple refinements needed for the wait loop.

Several issues need attention:

  1. Line 229-230: When the export completes naturally (!result.exporting && !result.cancelled), the code returns without updating the progress bar to 100% or printing a completion message. Users will see the progress stop at some percentage < 100% without confirmation.

  2. Line 232: The progress calculation doesn't validate the server's response. If result.progress is negative or > 1.0 (due to a bug or API mismatch), the cast to u64 could produce unexpected values.

  3. Line 238: The 10ms sleep is quite aggressive. Consider increasing to 100-500ms to reduce RPC server load while still providing responsive updates.

  4. No overall timeout: If the server becomes unresponsive during the export, this loop will poll indefinitely. Consider adding a maximum wait time (e.g., 12 hours) with a timeout message.

Apply this diff to address these concerns:

     pb.set_style(
         ProgressStyle::with_template(
             "[{elapsed_precise}] [{wide_bar}] {percent}% {msg} ",
         )
         .expect("indicatif template must be valid")
         .progress_chars("#>-"),
     );
+    let start = std::time::Instant::now();
+    let max_wait = Duration::from_secs(12 * 60 * 60); // 12 hours
     loop {
+        if start.elapsed() > max_wait {
+            pb.abandon_with_message("Export timed out");
+            anyhow::bail!("Export status polling timed out after {} hours", max_wait.as_secs() / 3600);
+        }
         let result = client
             .call(
                 ForestChainExportStatus::request(())?
                     .with_timeout(Duration::from_secs(30)),
             )
             .await?;
         if result.cancelled {
             pb.set_message("Export cancelled");
             pb.abandon();
 
             return Ok(());
         }
         if !result.exporting {
+            pb.set_position(10000);
+            pb.finish_with_message("Export completed");
             return Ok(());
         }
-        let position = (result.progress * 10000.0).trunc() as u64;
+        let position = (result.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
         pb.set_position(position);
 
         if position == 10000 {
             break;
         }
-        tokio::time::sleep(Duration::from_millis(10)).await;
+        tokio::time::sleep(Duration::from_millis(100)).await;
     }
     pb.finish_with_message("Export completed");
 
     return Ok(());
🧹 Nitpick comments (2)
src/cli/subcommands/snapshot_cmd.rs (2)

46-53: Consider adding --format json output option.

The new subcommands look good. However, as discussed in the previous comment thread, adding a --format json flag would facilitate bash scripting and automation. Currently, line 249 uses debug formatting ({:?}) for the non-wait case.

Consider adding a format flag to both ExportStatus and ExportCancel commands to support JSON output for better scriptability.


244-251: Improve non-wait status output formatting.

Line 249 uses debug formatting ({:?}) which is not user-friendly for a CLI tool. Consider either:

  1. Formatting the output in a human-readable way (e.g., "Export Status: In Progress (45%)" or "Export Status: Not Running")
  2. Implementing the --format json option discussed earlier to provide structured output

Example for human-readable output:

-    println!("{:?}", result);
+    if result.exporting {
+        println!("Export in progress: {:.1}%", result.progress * 100.0);
+    } else if result.cancelled {
+        println!("Export cancelled");
+    } else {
+        println!("No export in progress");
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee3bb9c and 820bcd5.

📒 Files selected for processing (1)
  • src/cli/subcommands/snapshot_cmd.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/cli/subcommands/snapshot_cmd.rs (2)
src/rpc/client.rs (2)
  • client (114-114)
  • request (272-285)
src/rpc/reflect/mod.rs (1)
  • request (250-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: All lint checks
  • GitHub Check: Build Ubuntu
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: Build MacOS
  • GitHub Check: Check
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: tests-release
  • GitHub Check: tests
🔇 Additional comments (3)
src/cli/subcommands/snapshot_cmd.rs (3)

10-10: LGTM!

The import of ApiExportResult is necessary for the new export status functionality.


173-183: LGTM!

The persist-before-checksum logic correctly addresses the previous review concern. The implementation properly handles both Done and Cancelled cases.


185-194: LGTM!

Clear user feedback for both completion and cancellation scenarios.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/rpc/methods/chain.rs (1)

503-521: Guard progress calculation against division by zero.

When mutex.initial_epoch is 0 (e.g., at startup or if not properly initialized), the division (mutex.epoch as f64) / (mutex.initial_epoch as f64) produces NaN, which will fail JSON serialization and break the API response.

Apply this diff to safely compute progress:

-        let progress = if mutex.initial_epoch == 0 {
-            0.0
-        } else {
-            1.0 - ((mutex.epoch as f64) / (mutex.initial_epoch as f64))
-        };
+        let progress = if mutex.initial_epoch > 0 {
+            let p = 1.0 - ((mutex.epoch as f64) / (mutex.initial_epoch as f64));
+            if p.is_finite() { p.clamp(0.0, 1.0) } else { 0.0 }
+        } else if mutex.exporting {
+            0.0
+        } else {
+            1.0
+        };

This handles the zero case and clamps the result to [0.0, 1.0] to prevent out-of-range values.

🧹 Nitpick comments (3)
src/cli/subcommands/snapshot_cmd.rs (2)

195-243: Refine wait-mode completion handling, progress validation, and add polling timeout.

The wait mode implementation has several areas that need refinement (some flagged in previous reviews):

  1. Line 230: When the export finishes (!result.exporting), the code returns immediately without updating the progress bar to 100% or printing a completion message. Users won't see clear confirmation.

  2. Line 232: The progress calculation (result.progress * 10000.0).trunc() as u64 doesn't validate result.progress. If the server returns invalid values (e.g., negative, > 1.0, or NaN), the cast could produce unexpected results.

  3. Missing overall timeout: The polling loop has no maximum duration. If the server becomes unresponsive during a long export, this will poll indefinitely.

Apply this diff to address these concerns:

     pb.set_style(
         ProgressStyle::with_template(
             "[{elapsed_precise}] [{wide_bar}] {percent}% {msg} ",
         )
         .expect("indicatif template must be valid")
         .progress_chars("#>-"),
     );
+    let start = std::time::Instant::now();
+    let max_wait = Duration::from_secs(12 * 60 * 60); // 12 hours max
     loop {
+        if start.elapsed() > max_wait {
+            pb.abandon_with_message("Export timed out");
+            anyhow::bail!("Export status polling timed out after {} hours", max_wait.as_secs() / 3600);
+        }
         let result = client
             .call(
                 ForestChainExportStatus::request(())?
                     .with_timeout(Duration::from_secs(30)),
             )
             .await?;
         if result.cancelled {
             pb.set_message("Export cancelled");
             pb.abandon();
 
             return Ok(());
         }
         if !result.exporting {
+            pb.set_position(10000);
+            pb.finish_with_message("Export completed");
             return Ok(());
         }
-        let position = (result.progress * 10000.0).trunc() as u64;
+        let position = (result.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
         pb.set_position(position);
 
         if position == 10000 {
             break;
         }
         tokio::time::sleep(Duration::from_millis(10)).await;
     }
     pb.finish_with_message("Export completed");
 
     return Ok(());

244-251: Consider adding JSON output format for programmatic use.

The current implementation prints the debug representation of ApiExportStatus. As noted in previous discussion, supporting a --format json flag would facilitate bash scripting and programmatic consumption of the status output.

Consider adding a format flag to the ExportStatus variant:

 ExportStatus {
     /// Wait until it completes and print progress.
     #[arg(long)]
     wait: bool,
+    /// Output format (json, text)
+    #[arg(long, default_value = "text")]
+    format: OutputFormat,
 },

Then conditionally serialize the result:

match format {
    OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&result)?),
    OutputFormat::Text => println!("{:?}", result),
}
src/rpc/methods/chain.rs (1)

421-438: Cancellation mechanism can miss or leak signals; consider CancellationToken.

The current global CANCEL_EXPORT.notify_waiters() pattern has race conditions flagged in previous reviews:

  1. Signal loss: If notify_waiters() fires before the export task reaches the CANCEL_EXPORT.notified() await point, the signal is missed.
  2. Signal leakage: Switching to notify_one() as a stopgap still allows a late notification to linger and cancel the next export.

Recommended: Use a per-export tokio_util::sync::CancellationToken:

  • Store an Arc<Mutex<Option<CancellationToken>>> for the active export
  • On start_export, create a new token, store it, and clone it into the tokio::select!
  • In ForestChainExportCancel, lock, take the token, call cancel(), and return true if a token existed

This ensures each export has its own cancellation lifecycle without signal loss or leakage.

As a minimal stopgap (still imperfect), replace notify_waiters() with notify_one() to reduce (but not eliminate) signal loss:

-            CANCEL_EXPORT.notify_waiters();
+            CANCEL_EXPORT.notify_one();

But please prioritize moving to CancellationToken to fully resolve both issues.

Also applies to: 473-482, 534-538

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee3bb9c and 19147cd.

📒 Files selected for processing (2)
  • src/cli/subcommands/snapshot_cmd.rs (4 hunks)
  • src/rpc/methods/chain.rs (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/rpc/methods/chain.rs (2)
src/ipld/util.rs (3)
  • cancel_export (57-61)
  • end_export (52-55)
  • start_export (43-50)
src/chain/mod.rs (3)
  • export (52-52)
  • export (58-67)
  • export_v2 (71-133)
src/cli/subcommands/snapshot_cmd.rs (2)
src/rpc/client.rs (2)
  • client (114-114)
  • request (272-285)
src/rpc/reflect/mod.rs (1)
  • request (250-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build MacOS
  • GitHub Check: Build Ubuntu
  • GitHub Check: All lint checks
  • GitHub Check: tests
  • GitHub Check: tests-release
  • GitHub Check: Build forest binaries on Linux AMD64
🔇 Additional comments (1)
src/rpc/methods/chain.rs (1)

385-385: Breaking API change: return type now ApiExportResult.

Both ForestChainExport (Line 385) and ChainExport (Line 609) have changed their return type from Option<String> to ApiExportResult. This is a breaking change for RPC clients that expect the old signature.

Ensure that:

  1. This change is documented in the CHANGELOG
  2. Clients are notified if this is part of a public API
  3. API versioning is considered if backward compatibility is required

Based on the PR description, this appears to be an intentional change as part of the feature implementation. Verify that dependent clients and integrations are updated accordingly.

Also applies to: 609-609

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/cli/subcommands/snapshot_cmd.rs (1)

164-166: Replace Duration::MAX with a finite RPC timeout.

Using Duration::MAX still risks overflow in the JSON-RPC timeout machinery (prior feedback flagged this as well). Please switch to a realistic upper bound (e.g., a named constant such as 6 h) and reuse it for the other export calls in this file.

-                let export_result = client
-                    .call(ForestChainExport::request((params,))?.with_timeout(Duration::MAX))
+                const EXPORT_TIMEOUT: Duration = Duration::from_secs(6 * 60 * 60); // 6h cap
+                let export_result = client
+                    .call(ForestChainExport::request((params,))?.with_timeout(EXPORT_TIMEOUT))

Please apply the same timeout constant to the non-wait export-status call and the diff export path below.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19147cd and a317d85.

📒 Files selected for processing (1)
  • src/cli/subcommands/snapshot_cmd.rs (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build MacOS
  • GitHub Check: Build Ubuntu
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: tests-release
  • GitHub Check: tests
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: All lint checks

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/cli/subcommands/snapshot_cmd.rs (1)

204-275: Honor --format json in --wait mode or reject the combination.

Currently format is ignored when --wait is set, always rendering a TTY bar. Either:

  • Stream JSON status objects each tick, or
  • Error out on --wait --format json with a clear message.

If you want, I can draft a minimal JSON-streaming loop (one-line JSON per tick) to keep it script-friendly. Would you prefer that?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8dc3cb6 and 4cfe6f3.

📒 Files selected for processing (1)
  • src/cli/subcommands/snapshot_cmd.rs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/cli/subcommands/snapshot_cmd.rs (2)
src/rpc/client.rs (2)
  • client (114-114)
  • request (272-285)
src/rpc/reflect/mod.rs (1)
  • request (250-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build MacOS
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build Ubuntu
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: tests-release
  • GitHub Check: tests
  • GitHub Check: All lint checks

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 302020b and 969831e.

📒 Files selected for processing (2)
  • src/chain/mod.rs (1 hunks)
  • src/rpc/methods/chain.rs (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/chain/mod.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/rpc/methods/chain.rs (1)
src/ipld/util.rs (3)
  • cancel_export (57-61)
  • end_export (52-55)
  • start_export (43-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build MacOS
  • GitHub Check: Build Ubuntu
  • GitHub Check: Build forest binaries on Linux AMD64
  • GitHub Check: tests-release
  • GitHub Check: tests
  • GitHub Check: All lint checks
🔇 Additional comments (2)
src/rpc/methods/chain.rs (2)

424-493: Export cancellation structure is sound, but depends on CancellationToken fix.

The pattern of racing the export against cancellation using tokio::select! is correct, and the lifecycle (start_export → select → end_export) is properly sequenced. However, this implementation depends on fixing the global CANCELLATION_TOKEN issue flagged above.

After applying the per-export token fix, verify that:

  1. Multiple sequential exports work correctly
  2. Cancelling one export doesn't affect subsequent exports
  3. Attempting to cancel when no export is running returns false

536-545: Cancellation logic is correct but depends on token fix.

The logic correctly detects an in-progress export by attempting to acquire the lock. However, with the current global CANCELLATION_TOKEN, calling cancel() breaks all future exports (see earlier comment).

After applying the per-export token fix, this logic will work as intended.

@elmattic elmattic requested a review from hanabi1224 October 10, 2025 16:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/cli/subcommands/snapshot_cmd.rs (1)

233-254: Add missing completion check in polling loop.

The loop checks for cancellation (lines 240-244) and breaks when position >= 10000 (lines 249-251), but doesn't explicitly check if the export has completed naturally. If result.exporting becomes false while progress is less than 1.0 (due to rounding or server behavior), the loop will continue polling indefinitely.

Apply this diff to add the completion check:

                     loop {
                         let result = client
                             .call(
                                 ForestChainExportStatus::request(())?
                                     .with_timeout(Duration::from_secs(30)),
                             )
                             .await?;
                         if result.cancelled {
                             pb.set_message("Export cancelled");
                             pb.abandon();
 
                             return Ok(());
                         }
+                        if !result.exporting {
+                            pb.set_position(10000);
+                            pb.finish_with_message("Export completed");
+                            return Ok(());
+                        }
                         let position = (result.progress.clamp(0.0, 1.0) * 10000.0).trunc() as u64;
                         pb.set_position(position);
 
                         if position >= 10000 {
                             break;
                         }
                         tokio::time::sleep(Duration::from_millis(500)).await;
                     }
                     pb.finish_with_message("Export completed");

Note: This issue was raised in a previous review but appears to still be present in the code.

🧹 Nitpick comments (1)
src/cli/subcommands/snapshot_cmd.rs (1)

53-60: Consider documenting JSON output format.

While the text format is self-explanatory, it would be helpful to document what fields are included in the JSON output (e.g., exporting, progress, cancelled, start_time) so users know what to expect when using --format json.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 119823f and 2c9c0a7.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • src/cli/subcommands/snapshot_cmd.rs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/cli/subcommands/snapshot_cmd.rs (2)
src/rpc/client.rs (2)
  • client (114-114)
  • request (272-285)
src/rpc/reflect/mod.rs (1)
  • request (250-260)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build Ubuntu
  • GitHub Check: cargo-publish-dry-run
  • GitHub Check: Build MacOS
  • GitHub Check: All lint checks
  • GitHub Check: tests-release
  • GitHub Check: tests
  • GitHub Check: Build forest binaries on Linux AMD64
🔇 Additional comments (3)
src/cli/subcommands/snapshot_cmd.rs (2)

23-27: LGTM: Format enum is properly defined.

The enum correctly derives the necessary traits for clap integration and the variants are appropriately named.


269-280: LGTM: ExportCancel implementation handles all cases correctly.

The implementation properly handles both success and failure cases, providing clear feedback to the user in each scenario.

CHANGELOG.md (1)

32-32: LGTM: Changelog entry is clear and follows conventions.

The entry properly documents the new subcommands and references the related issue.

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.

Snapshot export status/stop

2 participants