Skip to content

fix: require confirmation before restoring deleted local files in ht revert - #12

Open
aoruLola wants to merge 6 commits into
mainfrom
fix/revert-confirmation-v2
Open

fix: require confirmation before restoring deleted local files in ht revert#12
aoruLola wants to merge 6 commits into
mainfrom
fix/revert-confirmation-v2

Conversation

@aoruLola

Copy link
Copy Markdown
Collaborator

Summary

  • When a checked-out asset is deleted locally (local_hash == None, base_hash == Some), ht revert now treats this as overwriting a local change, triggering the dangerous-operation prompt unless --yes is provided
  • Previously this case fell through to false, silently recreating the file without confirmation
  • Also adds the ht revert command to recover a single asset from a previous snapshot

Test plan

  • Unit test deleted_local_file_treated_as_overwrite verifies the match expression logic
  • E2e tests verify revert workflow with staging, workspace updates, and lock release
  • All 32 CLI tests pass

…revert

When a checked-out asset is deleted locally (local_hash == None,
base_hash == Some), the revert command now treats this as overwriting
a local change, triggering the dangerous-operation prompt unless --yes
is provided. Previously this case fell through to false, silently
recreating the file without confirmation.

Also adds the `ht revert` command to recover a single asset from a
previous snapshot.

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

Copy link
Copy Markdown

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: 8606b6d7de

ℹ️ 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 thread crates/cli/src/cmd/revert.rs Outdated
args.to_changeset_id.as_deref(),
)
.await?;
let asset = find_snapshot_asset(&snapshot, &asset_path)?.clone();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stage deletions when the target snapshot lacks the asset

Reverting to a changeset before an asset was added should restore that asset to an absent state, but this lookup makes a missing target entry an error and exits before staging the deletion. In that scenario (ht revert --asset-path X --to <pre-add> with X present in the current workspace), users cannot use the new command to undo an added asset even though deletions are representable in the stage (blob_hash: None, as used by ht remove).

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/cmd/revert.rs Outdated
};
}

upsert_stage_asset(stage, asset_path, Some(blob_hash.to_string()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve snapshot asset IDs when staging reverts

When the matched snapshot entry has an asset_id different from its path, this stages only the path/blob hash via StageFile/AssetDelta. On submit the server defaults a missing asset ID to the path, so the revert updates or inserts a path-keyed asset instead of the snapshot asset, leaving the original asset ID unchanged; this affects repos populated through API/checkpoint flows that use stable asset IDs. The revert should carry asset.asset_id into the staged delta.

Useful? React with 👍 / 👎.

1. Stage deletions when target snapshot lacks the asset:
   - When reverting to a changeset before an asset was added, the
     command now stages a deletion (blob_hash: None) and removes the
     local file instead of erroring.

2. Preserve snapshot asset IDs when staging reverts:
   - Add asset_id field to AssetDelta with serde skip_serializing_if
   - Pass asset_id from snapshot entries through apply_revert_state
     and upsert_stage_asset into the staged delta
   - On submit the server defaults a missing asset ID to the path,
     so this preserves stable asset IDs from API/checkpoint flows

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

Copy link
Copy Markdown

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: ee163affcc

ℹ️ 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 thread crates/cli/src/cmd/revert.rs Outdated
&asset_path,
None,
base_hash.as_deref(),
None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Carry the current asset id when staging absent reverts

When the target snapshot lacks this path but the current branch contains it under a stable asset_id different from the path, this missing-target branch stages the deletion with asset_id: None. The submit path defaults a missing id to asset.path and removes that key (crates/server/src/core/versioning.rs:932 and :948), so the stable-id asset remains in the snapshot and the revert-to-absent is effectively not applied. Fresh evidence is that the newly added absent-target branch still passes None as the staged asset id.

Useful? React with 👍 / 👎.


update
}
None => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release locks after absent-target reverts

When --to points to a snapshot before the asset existed, execution enters this branch and completes the delete/stage flow without the !args.keep_lock lock-release call used by the restore branch above. Because --keep-lock is documented as the opt-out, the default ht revert --asset-path X --to <pre-add> leaves the HyperTide lock held after a successful revert and requires a manual ht lock release.

Useful? React with 👍 / 👎.

@aoruLola

Copy link
Copy Markdown
Collaborator Author

@codex

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

Copy link
Copy Markdown

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: d76c72392d

ℹ️ 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".

if let Some(lock) = state.lock_manager.get_lock(&asset.path) {
if let Some(lock) = state
.lock_manager
.get_lock_with_repo(repo_id, "asset", &asset.path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor legacy locks during repo-scoped submits

When upgrading with active locks created by the previous API, those rows are loaded with repo_id == "" (the old lock path used the default repo-less key). This submit-time check now only looks up locks under the requested repo, while the lock API also rejects empty repo IDs, so those legacy locks become invisible until they expire and another user can submit over an asset that was locked before the deploy. Add a migration/cleanup strategy or a temporary fallback check for repo-less locks.

Useful? React with 👍 / 👎.

&mut workspace,
&mut stage,
&asset_path,
None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve staged deletion status after revert

When the target snapshot does not contain the asset, this passes None into apply_revert_state, creating a staged deletion. The status path collapses staged state to staged_hash and only classifies a row as staged when that hash is Some, so after this revert ht status reports the asset as deleted/unmodified instead of showing the pending staged deletion that ht submit will apply. Carry an explicit staged-delete marker through status classification.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: 0f56f11638

ℹ️ 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 thread crates/server/src/routes.rs Outdated
if let Ok(identity) = rate_limit.auth_manager.validate_access_token(&token).await {
return format!("principal:{}", identity.owner_id);
}
return "anonymous".to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back to peer IP for invalid bearer tokens

When a request carries an expired or malformed Bearer token, this branch returns the literal anonymous bucket before reaching the peer-IP fallback used for invalid API keys below. In rate-limited deployments, unrelated clients with invalid/expired bearer tokens from different IPs all consume the same global bucket and can 429 each other (or be DoSed), instead of being isolated by the anonymous per-IP bucket.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: cac8f085d6

ℹ️ 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 +208 to +211
let peer_ip = request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|connect_info| connect_info.0.ip());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor forwarded client IPs before anonymous rate limiting

In reverse-proxy deployments (the self-hosting docs say to expose the TLS proxy rather than raw :3000), ConnectInfo is the proxy's socket address, not the original client. Because the previous X-Forwarded-For/X-Real-IP handling is gone, all anonymous or invalid-credential clients behind the proxy share one anonymous:<proxy-ip> bucket and can 429 each other. Consider using a trusted-forwarded-header path or proxy-aware configuration for this bucket.

Useful? React with 👍 / 👎.

Comment on lines +230 to +234
} else if let Some(api_key) = api_key {
if let Ok(identity) = rate_limit
.auth_manager
.validate_api_key_identity(&api_key)
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid auth DB lookups before rate-limit checks

With DB-backed auth, persisted API keys that are not in the in-memory cache (for example after a server restart) and random invalid X-API-Key values both call validate_api_key_identity before allow() is checked. A flood of invalid or unique keys therefore still performs one auth lookup per request even after the anonymous bucket is exhausted, so rate limiting no longer shields the database; add a cheap pre-limit/cache or defer DB validation until after a coarse bucket check.

Useful? React with 👍 / 👎.

Comment thread crates/cli/src/cmd/revert.rs Outdated
base_hash: Option<&str>,
asset_id: Option<String>,
) -> RevertStateUpdate {
if base_hash == blob_hash {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stage asset-id-only reverts instead of clearing them

When the target snapshot has the same blob hash at this path but a different asset_id (e.g. an asset was delete/re-added with identical bytes), this hash-only comparison treats the revert as a no-op, removes any staged delta, and even rewrites the local workspace metadata to the old id. The server snapshot remains at the current id, and subsequent CLI updates/deletes can target the wrong asset id, so this needs to compare/stage identity changes as well as blob changes.

Useful? React with 👍 / 👎.

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

Copy link
Copy Markdown

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: bb488ec54c

ℹ️ 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 thread crates/cli/src/utils.rs
let payload = LockRequest { file_path: path };
let repo_id = match repo_id {
Some(repo_id) => repo_id.to_string(),
None => resolve_repo(&profile, None)?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve lock repo from the workspace when profile has none

When a user logs in without a default repo and then initializes a workspace with ht checkout --repo repo-a, the lock subcommands have no --repo flag and pass None into this helper, so this new repo-scoped payload path fails with repo not set before contacting the server. Other workspace commands fall back to workspace.repo_id; the lock commands need the same fallback (or an explicit repo option), otherwise locks are unusable in a valid checked-out workspace unless the user separately mutates the profile.

Useful? React with 👍 / 👎.

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.

1 participant