mirror of
https://github.com/jj-vcs/jj.git
synced 2026-07-03 22:24:35 +08:00
run: Add --clean flag to wipe working copies before each job
--clean deletes each workspace's working_copy/ and state/ directories after acquiring the lock, so every commit starts from a freshly checked-out tree. Build artifacts from previous runs are not reused.
This commit is contained in:
@@ -167,6 +167,9 @@ struct WorkspacePool {
|
||||
/// rewritten commit. Loaded once from `snapshot.auto-track`; essentially
|
||||
/// the user's `.gitignore` story for what counts as a build artifact.
|
||||
auto_tracking_matcher: Box<dyn Matcher>,
|
||||
/// When true, wipe each slot's working copy on acquisition so every commit
|
||||
/// starts from a freshly checked-out tree (no artifact reuse).
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
impl WorkspacePool {
|
||||
@@ -174,6 +177,7 @@ impl WorkspacePool {
|
||||
repo_path: &Path,
|
||||
size: NonZeroUsize,
|
||||
auto_tracking_matcher: Box<dyn Matcher>,
|
||||
clean: bool,
|
||||
) -> Result<Self, RunError> {
|
||||
// The parent() call is needed to not write under `.jj/repo/`.
|
||||
let base_path = repo_path.parent().unwrap().join("run").join("default");
|
||||
@@ -182,6 +186,7 @@ impl WorkspacePool {
|
||||
base_path,
|
||||
size,
|
||||
auto_tracking_matcher,
|
||||
clean,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -209,7 +214,7 @@ impl WorkspacePool {
|
||||
|
||||
let is_reused_workspace = tree_state_path.exists();
|
||||
let settings = default_tree_state_settings();
|
||||
let mut tree_state = if is_reused_workspace {
|
||||
let mut tree_state = if !self.clean && is_reused_workspace {
|
||||
// Load the persisted tree state so `check_out` below can diff
|
||||
// against it, only touching files that changed and removing files
|
||||
// no longer present in the new tree.
|
||||
@@ -227,10 +232,15 @@ impl WorkspacePool {
|
||||
fs::remove_file(&tree_state_path)?;
|
||||
ts
|
||||
} else {
|
||||
// First use, or the previous job crashed before saving. Wipe any
|
||||
// leftover working copy so we start from a clean slate, then use
|
||||
// an in-memory empty tree state. `tree_state` stays absent on disk
|
||||
// until a job writes it via `persist()`.
|
||||
// This is the first use of the workspace, the previous job crashed,
|
||||
// or --clean was passed. Wipe any leftover working copy so we start
|
||||
// from a clean slate, then use an in-memory empty tree state.
|
||||
// `tree_state` stays absent on disk until a successful job writes
|
||||
// it via `persist()`.
|
||||
fs::remove_file(&tree_state_path).or_else(|e| match e {
|
||||
e if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
e => Err(RunError::PathDeletionFailure(tree_state_path.clone(), e)),
|
||||
})?;
|
||||
fs::remove_dir_all(&working_copy_dir).or_else(|e| match e {
|
||||
e if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
||||
e => Err(RunError::PathDeletionFailure(working_copy_dir.clone(), e)),
|
||||
@@ -586,6 +596,14 @@ pub struct RunArgs {
|
||||
/// from the subdirectory `jj run` was invoked from.
|
||||
#[arg(long)]
|
||||
root: bool,
|
||||
|
||||
/// Delete each working copy before running the command
|
||||
///
|
||||
/// By default `jj run` reuses working copies between invocations so build
|
||||
/// artifacts are preserved. With `--clean`, every commit starts from a
|
||||
/// freshly checked-out tree.
|
||||
#[arg(long)]
|
||||
clean: bool,
|
||||
}
|
||||
|
||||
/// Precedence: `--jobs`, `run.jobs` config, 1.
|
||||
@@ -680,7 +698,12 @@ pub async fn cmd_run(
|
||||
let mut done_commits = HashSet::new();
|
||||
let (sender_tx, mut receiver) = mpsc::channel(jobs.get());
|
||||
|
||||
let pool = Arc::new(WorkspacePool::new(&repo_path, jobs, auto_tracking_matcher)?);
|
||||
let pool = Arc::new(WorkspacePool::new(
|
||||
&repo_path,
|
||||
jobs,
|
||||
auto_tracking_matcher,
|
||||
args.clean,
|
||||
)?);
|
||||
let stored_len = resolved_commits.len();
|
||||
|
||||
let spec = Arc::new(CommandSpec {
|
||||
|
||||
@@ -3000,6 +3000,9 @@ $ jj run -j 4 -- pre-commit run .github/pre-commit.yaml
|
||||
|
||||
Overrides the `run.jobs` config setting. Defaults to 1 if neither is set.
|
||||
* `--root` — Run the command from the working-copy root in each commit instead of from the subdirectory `jj run` was invoked from
|
||||
* `--clean` — Delete each working copy before running the command
|
||||
|
||||
By default `jj run` reuses working copies between invocations so build artifacts are preserved. With `--clean`, every commit starts from a freshly checked-out tree.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -947,61 +947,6 @@ fn test_run_pool_removes_file_absent_in_next_commit() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Files tracked into a commit by run 1 must not reappear in a different
|
||||
/// commit processed by the same pool slot in run 2.
|
||||
///
|
||||
/// Run 1 rewrites commit2 (adding artifact.txt). The slot's saved tree_state
|
||||
/// therefore records {seed.txt, artifact.txt}. Run 2 targets commit1 whose
|
||||
/// tree is {seed.txt}; the checkout diff removes artifact.txt from the slot,
|
||||
/// so commit1's rewrite must not contain it.
|
||||
#[test]
|
||||
fn test_run_pool_no_file_leak_between_invocations() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
|
||||
let work_dir = test_env.work_dir("repo");
|
||||
|
||||
// commit1: seed.txt only.
|
||||
work_dir.write_file("seed.txt", "seed");
|
||||
work_dir.run_jj(&["commit", "-m", "commit1"]).success();
|
||||
// commit2: descends from commit1, no extra files (same tree).
|
||||
work_dir.run_jj(&["commit", "-m", "commit2"]).success();
|
||||
// Stack: root → commit1 (seed.txt) → commit2 (seed.txt) → @
|
||||
|
||||
// Run 1: produce artifact.txt in commit2 (@-) so the slot's saved
|
||||
// tree_state records {seed.txt, artifact.txt}.
|
||||
// Stack before run 1: root → commit1 (@--) → commit2 (@-) → @
|
||||
work_dir
|
||||
.run_jj(&[
|
||||
"run",
|
||||
"--config",
|
||||
"run.jobs=1",
|
||||
"-r",
|
||||
"@-",
|
||||
"--",
|
||||
"touch",
|
||||
"artifact.txt",
|
||||
])
|
||||
.success();
|
||||
// After run 1: root → commit1 (@--) → commit2' (@-) → @ (rebased).
|
||||
// commit2' now has {seed.txt, artifact.txt}.
|
||||
|
||||
// Run 2: no-op on commit1 (@--). Pool reuses the slot whose tree_state
|
||||
// says {seed.txt, artifact.txt}. The checkout diff removes artifact.txt,
|
||||
// so commit1's rewrite must not contain it.
|
||||
work_dir
|
||||
.run_jj(&["run", "--config", "run.jobs=1", "-r", "@--", "--", "true"])
|
||||
.success();
|
||||
// After run 2: root → commit1' (@--) → commit2'' (@-) → @ (rebased).
|
||||
|
||||
assert_snapshot!(
|
||||
work_dir.run_jj(&["file", "list", "-r", "@--"]).success().stdout,
|
||||
@r"
|
||||
seed.txt
|
||||
[EOF]
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
/// When a pool slot is reused, files left on disk that the previous commit's
|
||||
/// .gitignore excluded must be removed before the next commit runs. Otherwise
|
||||
/// the next commit (which may not ignore the same paths) would pick them up at
|
||||
@@ -1077,6 +1022,29 @@ fn test_run_pool_removes_previously_ignored_files() {
|
||||
// unnecessarily remove files that are still ignored
|
||||
assert!(fs::exists(run_wc.join("always_ignored.txt")).unwrap());
|
||||
assert!(!fs::exists(run_wc.join("ignored.txt")).unwrap());
|
||||
|
||||
// Passing --clean removes even always-ignored files
|
||||
work_dir
|
||||
.run_jj(&[
|
||||
"run",
|
||||
"-r=a",
|
||||
"--clean",
|
||||
"--",
|
||||
&fake_formatter_path,
|
||||
"--stdout",
|
||||
"done",
|
||||
])
|
||||
.success();
|
||||
assert_snapshot!(
|
||||
work_dir.run_jj(&["file", "list", "-r", "a"]),
|
||||
@r"
|
||||
.gitignore
|
||||
seed.txt
|
||||
[EOF]
|
||||
"
|
||||
);
|
||||
assert!(!fs::exists(run_wc.join("always_ignored.txt")).unwrap());
|
||||
assert!(!fs::exists(run_wc.join("ignored.txt")).unwrap());
|
||||
}
|
||||
|
||||
/// A slot whose tree_state is absent (simulating a crash mid-job) should be
|
||||
|
||||
Reference in New Issue
Block a user