Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ DB_PORT=$(trop reserve --tag db)

As with `trop reserve`, these reservations will be associated with the current directory, and thus will be automatically pruned when the directory is removed.

### Releasing reservations

`trop release` without a tag filter removes every tagged and untagged
reservation at exactly the resolved path in one transaction. Descendant paths
are left alone unless `--recursive` is supplied.

Use `--tag <TAG>` to remove only that tagged reservation, or
`--untagged-only` to remove only the untagged reservation. The two filters are
mutually exclusive, and a filter with no match succeeds as an idempotent no-op.
The same filter selects matching rows below the path when combined with
`--recursive`.

Release follows the standard path guard: the target must be the current
directory, an ancestor, or a descendant. A sideways unrelated path is rejected
before mutation unless `--allow-unrelated-path`, the corresponding effective
configuration permission, or `--force` authorizes it.

For recurring reservation patterns, you add a "tropfile" (`trop.yaml`) file to your project root, which can then define a "reservation group" like so:

```yaml
Expand Down
28 changes: 28 additions & 0 deletions trop-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,34 @@ phase. Normal mode reports only aggregate prune/expire counts on stderr, while
Exhaustion errors distinguish skipped cleanup from an attempted cleanup and
report that the remaining ports are reserved, excluded, or occupied.

### Releasing Reservations

Without a tag filter, `trop release` removes every tagged and untagged
reservation at exactly the resolved path. Planning and deletion for that exact
path share one transaction, so a failure cannot leave only part of the
exact-path set deleted. Descendant reservations remain unless `--recursive` is
supplied.

```bash
# Release every reservation for the current directory
trop release

# Release one tagged reservation
trop release --tag web

# Release only the untagged reservation
trop release --untagged-only
```

`--tag` and `--untagged-only` are mutually exclusive. A selector with no match
succeeds as an idempotent no-op, and either selector can be combined with
`--recursive` to select matching descendant rows.

The target path must be the current directory, an ancestor, or a descendant.
Use `--allow-unrelated-path` to bypass only that relationship check, or
`--force` to authorize the release despite it. The effective
`allow_unrelated_path` configuration permission is honored as well.

### Use in Build Scripts

Example `justfile`:
Expand Down
11 changes: 9 additions & 2 deletions trop-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,15 @@ impl Command {
);
ConfigScope::Discover
}
Self::Release(_)
| Self::Prune(_)
Self::Release(command) => {
set_true(
&mut command_line,
command.allow_unrelated_path,
ConfigField::AllowUnrelatedPath,
);
ConfigScope::Discover
}
Self::Prune(_)
| Self::AssertReservation(_)
| Self::AssertPort(_)
| Self::Exclude(_)
Expand Down
23 changes: 19 additions & 4 deletions trop-cli/src/commands/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub struct ReleaseCommand {
#[arg(long)]
pub force: bool,

/// Allow operations on unrelated paths
#[arg(long)]
pub allow_unrelated_path: bool,

/// Perform a dry run
#[arg(long)]
pub dry_run: bool,
Expand All @@ -51,11 +55,18 @@ impl ReleaseCommand {
));
}

// 3. Open the database from the shared effective configuration.
// 3. Consume the path permission from the shared effective configuration.
let allow_unrelated_path = context.effective()?.allow_unrelated_path();

// 4. Open the database from the shared effective configuration.
let mut db = context.open_database()?;

// 5. Handle recursive release or single release
if self.recursive {
if !self.force && !allow_unrelated_path {
Database::validate_path_relationship(&path, false).map_err(CliError::from)?;
}

// For recursive release, we need to find all reservations under this path
// and release them one by one
let all_reservations =
Expand Down Expand Up @@ -84,7 +95,9 @@ impl ReleaseCommand {
// Build release options for this reservation
let options = ReleaseOptions::new(reservation.key().clone())
.with_force(self.force)
.with_allow_unrelated_path(true); // Already validated
// Validate the requested recursive root once. Descendant keys
// can be sideways from the CWD even when that root is an ancestor.
.with_allow_unrelated_path(true);

// Build plan using database connection for reading
let plan = ReleasePlan::new(options)
Expand Down Expand Up @@ -122,20 +135,22 @@ impl ReleaseCommand {
}
}
} else {
// Single release: build key and release it
// Exact release: an omitted filter selects every tag at this path.
let release_all_exact_path_tags = self.tag.is_none() && !self.untagged_only;
let tag = if self.untagged_only { None } else { self.tag };

let key = ReservationKey::new(path, tag)
.map_err(|e| CliError::InvalidArguments(e.to_string()))?;

let options = ReleaseOptions::new(key)
.with_force(self.force)
.with_allow_unrelated_path(true); // Path was resolved from CWD
.with_allow_unrelated_path(allow_unrelated_path);

// Begin transaction for single release
let tx = db.begin_transaction().map_err(CliError::from)?;

let plan = ReleasePlan::new(options)
.with_all_exact_path_tags(release_all_exact_path_tags)
.build_plan(&tx)
.map_err(CliError::from)?;

Expand Down
8 changes: 6 additions & 2 deletions trop-cli/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ impl TestEnv {
///
/// Runs `trop release` for the given path.
pub fn release(&self, path: &Path) {
self.command()
let mut command = self.command();
command
.current_dir(&self.temp_path)
.arg("release")
.arg("--path")
.arg(path)
Expand All @@ -214,7 +216,9 @@ impl TestEnv {

/// Release a reservation with a tag.
pub fn release_with_tag(&self, path: &Path, tag: &str) {
self.command()
let mut command = self.command();
command
.current_dir(&self.temp_path)
.arg("release")
.arg("--path")
.arg(path)
Expand Down
3 changes: 3 additions & 0 deletions trop-cli/tests/error_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fn test_success_exit_code() {
.arg("release")
.arg("--path")
.arg(&test_path)
.current_dir(env.path())
.assert()
.code(0);
}
Expand Down Expand Up @@ -771,6 +772,7 @@ fn test_release_idempotent_success() {
.arg("release")
.arg("--path")
.arg(&test_path)
.current_dir(env.path())
.output()
.unwrap();

Expand Down Expand Up @@ -874,6 +876,7 @@ fn test_release_nonexistent_clear_message() {
.arg("release")
.arg("--path")
.arg(&test_path)
.current_dir(env.path())
.output()
.unwrap();

Expand Down
2 changes: 2 additions & 0 deletions trop-cli/tests/global_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ fn test_verbose_flag_works_with_all_commands() {
.arg("release")
.arg("--path")
.arg(&test_path)
.current_dir(env.path())
.assert()
.success();
}
Expand Down Expand Up @@ -207,6 +208,7 @@ fn test_quiet_flag_works_with_all_commands() {
.arg("release")
.arg("--path")
.arg(&test_path)
.current_dir(env.path())
.output()
.unwrap();
assert!(release_out.status.success());
Expand Down
Loading
Loading