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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,19 @@ On btrfs, exact copies use writable subvolume snapshots and filtered copies use

When the workspace is a Git repository, the new workspace has detached `HEAD` and retains index and working-tree state.

If the source contains `.rift.toml`, `rift create` runs configured postclone hooks after the workspace is copied, registered, and prepared. Use `--no-hooks` to skip them.
If the source contains `.rift.toml`, `rift create` runs configured postcreate hooks after the workspace is created, registered, and prepared. Use `--no-hooks` to skip them.

```toml
version = 1

[[hooks.postclone]]
[[hooks.postcreate]]
run = "pnpm install --frozen-lockfile"

[[hooks.postclone]]
[[hooks.postcreate]]
run = "pnpm run codegen"
```

Postclone commands run in the new workspace root. If a hook fails, the workspace remains registered and `rift create` exits with an error.
Postcreate commands run in the new workspace root. If a hook fails, the workspace remains registered and `rift create` exits with an error.

### List And Ancestors

Expand Down
36 changes: 18 additions & 18 deletions crates/core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};

#[derive(Debug, Default)]
pub(crate) struct Config {
postclone: Vec<Postclone>,
postcreate: Vec<Postcreate>,
}

impl Config {
Expand All @@ -17,17 +17,17 @@ impl Config {
parse(&path, &fs::read_to_string(&path)?)
}

pub(crate) fn postclone(&self) -> &[Postclone] {
&self.postclone
pub(crate) fn postcreate(&self) -> &[Postcreate] {
&self.postcreate
}
}

#[derive(Clone, Debug)]
pub(crate) struct Postclone {
pub(crate) struct Postcreate {
run: String,
}

impl Postclone {
impl Postcreate {
pub(crate) fn run(&self) -> &str {
&self.run
}
Expand All @@ -45,12 +45,12 @@ struct RawConfig {
#[serde(deny_unknown_fields)]
struct RawHooks {
#[serde(default)]
postclone: Vec<RawPostclone>,
postcreate: Vec<RawPostcreate>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawPostclone {
struct RawPostcreate {
run: String,
}

Expand All @@ -64,18 +64,18 @@ fn parse(path: &Path, contents: &str) -> Result<Config> {
));
}
raw.hooks
.postclone
.postcreate
.into_iter()
.map(|step| {
let run = step.run.trim().to_owned();
if run.is_empty() {
Err(invalid_config(path, "postclone run cannot be empty"))
Err(invalid_config(path, "postcreate run cannot be empty"))
} else {
Ok(Postclone { run })
Ok(Postcreate { run })
}
})
.collect::<Result<Vec<_>>>()
.map(|postclone| Config { postclone })
.map(|postcreate| Config { postcreate })
}

fn invalid_config(path: &Path, message: impl Into<String>) -> Error {
Expand All @@ -90,26 +90,26 @@ mod tests {
use super::*;

#[test]
fn parses_ordered_postclone_steps() {
fn parses_ordered_postcreate_steps() {
let config = parse(
Path::new(".rift.toml"),
r#"
version = 1

[[hooks.postclone]]
[[hooks.postcreate]]
run = "echo one"

[[hooks.postclone]]
[[hooks.postcreate]]
run = "echo two"
"#,
)
.unwrap();

assert_eq!(
config
.postclone()
.postcreate()
.iter()
.map(Postclone::run)
.map(Postcreate::run)
.collect::<Vec<_>>(),
vec!["echo one", "echo two"]
);
Expand All @@ -123,7 +123,7 @@ run = "echo two"
r#"
version = 1

[[hooks.postclone]]
[[hooks.postcreate]]
run = " "
"#,
),
Expand All @@ -139,7 +139,7 @@ run = " "
r#"
version = 1

[[hooks.postclone]]
[[hooks.postcreate]]
run = "echo ok"
shell = "sh"
"#,
Expand Down
8 changes: 4 additions & 4 deletions crates/core/src/hook.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
use crate::config::Postclone;
use crate::config::Postcreate;
use crate::id::RiftId;
use crate::{Error, Result};
use std::path::Path;
use std::process::Command;

pub(crate) fn run_postclone(
steps: &[Postclone],
pub(crate) fn run_postcreate(
steps: &[Postcreate],
source: &Path,
destination: &Path,
id: &RiftId,
parent_id: &RiftId,
) -> Result<()> {
steps
.iter()
.map(Postclone::run)
.map(Postcreate::run)
.try_for_each(|command| run_step(command, source, destination, id, parent_id))
}

Expand Down
4 changes: 2 additions & 2 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ pub enum Error {
InsideSource(PathBuf),
#[error("invalid rift config at {path}: {message}")]
InvalidConfig { path: PathBuf, message: String },
#[error("postclone hook failed at {path}: `{command}` {message}")]
#[error("postcreate hook failed at {path}: `{command}` {message}")]
HookFailed {
path: PathBuf,
command: String,
Expand Down Expand Up @@ -250,7 +250,7 @@ impl Manager {
let _ = self.strategy.remove_directory(&destination);
}
result?;
hook::run_postclone(config.postclone(), &from, &destination, &id, &source.id)?;
hook::run_postcreate(config.postcreate(), &from, &destination, &id, &source.id)?;
Ok(destination)
}

Expand Down
14 changes: 7 additions & 7 deletions crates/core/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,18 @@ fn create_copy_all_preserves_regenerable_artifacts() {
}

#[test]
fn create_runs_postclone_hooks_in_destination_order() {
fn create_runs_postcreate_hooks_in_destination_order() {
let temp = TempDir::new().unwrap();
let source = source(&temp);
fs::write(
source.join(".rift.toml"),
r#"
version = 1

[[hooks.postclone]]
[[hooks.postcreate]]
run = "echo first >> hook.log"

[[hooks.postclone]]
[[hooks.postcreate]]
run = "echo second >> hook.log"
"#,
)
Expand All @@ -212,21 +212,21 @@ run = "echo second >> hook.log"
}

#[test]
fn postclone_failure_leaves_registered_workspace() {
fn postcreate_failure_leaves_registered_workspace() {
let temp = TempDir::new().unwrap();
let source = source(&temp);
fs::write(
source.join(".rift.toml"),
r#"
version = 1

[[hooks.postclone]]
[[hooks.postcreate]]
run = "echo before >> hook.log"

[[hooks.postclone]]
[[hooks.postcreate]]
run = "exit 7"

[[hooks.postclone]]
[[hooks.postcreate]]
run = "echo after >> hook.log"
"#,
)
Expand Down
6 changes: 3 additions & 3 deletions specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Default behavior:
- Copy the workspace while excluding known heavyweight regenerable dependency, build, and cache artifacts.
- Preserve manifests, lockfiles, dirty files, staged files, untracked files, and ignored files that are not part of the built-in excluded artifact set.
- `copyAll` opts into exact copying, including dependency and build artifacts.
- `hooks` defaults to true and runs `.rift.toml` postclone hooks after copy, Git preparation, and registry insertion. `hooks: false` skips config loading and hook execution.
- `hooks` defaults to true and runs `.rift.toml` postcreate hooks after workspace creation, Git preparation, and registry insertion. `hooks: false` skips config loading and hook execution.
- Detach `HEAD` in the new workspace.
- Return the path of the new workspace.

Expand All @@ -57,11 +57,11 @@ Default excluded artifacts are matched at any depth and include `node_modules`,
```toml
version = 1

[[hooks.postclone]]
[[hooks.postcreate]]
run = "pnpm install --frozen-lockfile"
```

Postclone hooks run sequentially in the destination workspace with inherited stdio and environment plus `RIFT_SOURCE`, `RIFT_DESTINATION`, `RIFT_ID`, and `RIFT_PARENT_ID`. The first failing command stops later hooks. The created workspace remains registered and on disk, and the create operation reports a hook failure with the destination path.
Postcreate hooks run sequentially in the destination workspace with inherited stdio and environment plus `RIFT_SOURCE`, `RIFT_DESTINATION`, `RIFT_ID`, and `RIFT_PARENT_ID`. The first failing command stops later hooks. The created workspace remains registered and on disk, and the create operation reports a hook failure with the destination path.

On btrfs, `from` must already be a subvolume. If it is an ordinary directory, fail and instruct the user to run `rift init` first. On other reflink-capable Linux filesystems, clone the directory tree with native per-file reflinks.

Expand Down
Loading