cli: add a command for updating a stale working copy

When a workspace's working-copy commit is updated from another
workspace, the workspace becomes "stale". That means that the working
copy on disk doesn't represent the commit that the repo's view says it
should. In this state, we currently automatically it to the desired
commit next time the user runs any command in the workspace. That can
be undesirable e.g. if the user had a slow build or test run started
in the working copy. It can also be surprising that a checkout happens
when the user ran a seemingly readonly command like `jj status`.

This patch makes most commands instead error out if the working copy
is stale, and adds a `jj workspace update-stale` to update it. The
user can still run commands with `--no-commit-working-copy` in this
state (doing e.g. `jj --no-commit-working-copy rebase -r @ -d @--` is
another way of getting into the stale-working-copy state, by the way).
This commit is contained in:
Martin von Zweigbergk 2022-10-02 10:09:46 -07:00 committed by Martin von Zweigbergk
parent e1be0f5096
commit a0573b1737
5 changed files with 118 additions and 36 deletions

View File

@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Adjusted precedence of revset union/intersection/difference operators.
`x | y & z` is now equivalent to `x | (y & z)`.
* Support for open commits has been dropped. The `ui.enable-open-commits` config
that was added in 0.5.0 is no longer respected. The `jj open/close` commands
have been deleted.
@ -25,6 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
description, even if there already was a description set. It now also only
works on the working-copy commit (there's no `-r` argument).
* If a workspace's working-copy commit has been updated from another workspace,
most commands in that workspace will now fail. Use the new
`jj workspace update-stale` command to update the workspace to the new
working-copy commit. (The old behavior was to automatically update the
workspace.)
### New features
* Commands with long output are paginated.

View File

@ -75,3 +75,11 @@ while you continue developing in another, for example.
When you're done using a workspace, use `jj workspace forget` to make the repo
forget about it. The files can be deleted from disk separately (either before or
after).
### Stale working copy
When you modify workspace A's working-copy commit from workspace B, workspace
A's working copy will become stale. By "stale", we mean that the files in the
working copy don't match the desired commit indicated by the `@` symbol in
`jj log`. When that happens, use `jj workspace update-stale` to update the files
in the working copy.

View File

@ -275,7 +275,7 @@ jj init --git-repo=.",
})
}
fn resolve_operation(
pub fn resolve_operation(
&self,
ui: &mut Ui,
workspace: Workspace,
@ -386,7 +386,7 @@ impl WorkspaceCommandHelper {
})
}
fn check_working_copy_writable(&self) -> Result<(), CommandError> {
pub fn check_working_copy_writable(&self) -> Result<(), CommandError> {
if self.may_update_working_copy {
Ok(())
} else {
@ -502,7 +502,7 @@ impl WorkspaceCommandHelper {
self.workspace.working_copy()
}
pub fn start_working_copy_mutation(
pub fn unsafe_start_working_copy_mutation(
&mut self,
) -> Result<(LockedWorkingCopy, Commit), CommandError> {
self.check_working_copy_writable()?;
@ -514,10 +514,17 @@ impl WorkspaceCommandHelper {
};
let locked_working_copy = self.workspace.working_copy_mut().start_mutation();
Ok((locked_working_copy, wc_commit))
}
pub fn start_working_copy_mutation(
&mut self,
) -> Result<(LockedWorkingCopy, Commit), CommandError> {
let (locked_working_copy, wc_commit) = self.unsafe_start_working_copy_mutation()?;
if wc_commit.tree_id() != locked_working_copy.old_tree_id() {
return Err(user_error("Concurrent working copy operation. Try again."));
}
Ok((locked_working_copy, wc_commit))
}
@ -680,41 +687,36 @@ impl WorkspaceCommandHelper {
};
let base_ignores = self.base_ignores();
let mut locked_wc = self.workspace.working_copy_mut().start_mutation();
let old_op_id = locked_wc.old_operation_id().clone();
let wc_commit = repo.store().get_commit(&wc_commit_id)?;
self.repo = match check_stale_working_copy(&locked_wc, &wc_commit, repo.clone()) {
Ok(repo) => repo,
Err(StaleWorkingCopyError::WorkingCopyStale) => {
// Update the working copy to what the view says.
writeln!(
ui,
"The working copy is stale (not updated since operation {}), now updating to \
operation {}",
short_operation_hash(locked_wc.old_operation_id()),
short_operation_hash(repo.op_id()),
)?;
locked_wc.check_out(&wc_commit.tree()).map_err(|err| {
CommandError::InternalError(format!(
"Failed to check out commit {}: {}",
wc_commit.id().hex(),
err
))
})?;
repo
locked_wc.discard();
return Err(user_error_with_hint(
format!(
"The working copy is stale (not updated since operation {}).",
short_operation_hash(&old_op_id)
),
"Run `jj workspace update-stale` to update it.",
));
}
Err(StaleWorkingCopyError::SiblingOperation) => {
locked_wc.discard();
return Err(CommandError::InternalError(format!(
"The repo was loaded at operation {}, which seems to be a sibling of the \
working copy's operation {}",
short_operation_hash(repo.op_id()),
short_operation_hash(locked_wc.old_operation_id())
short_operation_hash(&old_op_id)
)));
}
Err(StaleWorkingCopyError::UnrelatedOperation) => {
locked_wc.discard();
return Err(CommandError::InternalError(format!(
"The repo was loaded at operation {}, which seems unrelated to the working \
copy's operation {}",
short_operation_hash(repo.op_id()),
short_operation_hash(locked_wc.old_operation_id())
short_operation_hash(&old_op_id)
)));
}
};
@ -901,7 +903,7 @@ pub enum StaleWorkingCopyError {
UnrelatedOperation,
}
fn check_stale_working_copy(
pub fn check_stale_working_copy(
locked_wc: &LockedWorkingCopy,
wc_commit: &Commit,
repo: Arc<ReadonlyRepo>,
@ -1142,7 +1144,7 @@ pub fn resolve_base_revs(
}
}
fn update_working_copy(
pub fn update_working_copy(
ui: &mut Ui,
repo: &Arc<ReadonlyRepo>,
workspace_id: &WorkspaceId,

View File

@ -54,9 +54,9 @@ use maplit::{hashmap, hashset};
use pest::Parser;
use crate::cli_util::{
print_checkout_stats, print_failed_git_export, resolve_base_revs, short_commit_description,
short_commit_hash, user_error, user_error_with_hint, write_commit_summary, Args, CommandError,
CommandHelper, RevisionArg, WorkspaceCommandHelper,
check_stale_working_copy, print_checkout_stats, print_failed_git_export, resolve_base_revs,
short_commit_description, short_commit_hash, user_error, user_error_with_hint,
write_commit_summary, Args, CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper,
};
use crate::config::FullCommandArgs;
use crate::formatter::{Formatter, PlainTextFormatter};
@ -793,6 +793,7 @@ enum WorkspaceCommands {
Add(WorkspaceAddArgs),
Forget(WorkspaceForgetArgs),
List(WorkspaceListArgs),
UpdateStale(WorkspaceUpdateStaleArgs),
}
/// Add a workspace
@ -822,6 +823,13 @@ struct WorkspaceForgetArgs {
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceListArgs {}
/// Update a workspace that has become stale
///
/// For information about stale working copies, see
/// https://github.com/martinvonz/jj/blob/main/docs/working-copy.md.
#[derive(clap::Args, Clone, Debug)]
struct WorkspaceUpdateStaleArgs {}
/// Manage which paths from the working-copy commit are present in the working
/// copy
#[derive(clap::Args, Clone, Debug)]
@ -3850,6 +3858,9 @@ fn cmd_workspace(
WorkspaceCommands::List(command_matches) => {
cmd_workspace_list(ui, command, command_matches)
}
WorkspaceCommands::UpdateStale(command_matches) => {
cmd_workspace_update_stale(ui, command, command_matches)
}
}
}
@ -3982,6 +3993,49 @@ fn cmd_workspace_list(
Ok(())
}
fn cmd_workspace_update_stale(
ui: &mut Ui,
command: &CommandHelper,
_args: &WorkspaceUpdateStaleArgs,
) -> Result<(), CommandError> {
let workspace = command.load_workspace(ui)?;
let mut workspace_command = command.resolve_operation(ui, workspace)?;
let repo = workspace_command.repo().clone();
let workspace_id = workspace_command.workspace_id();
let (mut locked_wc, desired_wc_commit) =
workspace_command.unsafe_start_working_copy_mutation()?;
match check_stale_working_copy(&locked_wc, &desired_wc_commit, repo.clone()) {
Ok(_) => {
locked_wc.discard();
ui.write("Nothing to do (the working copy is not stale).\n")?;
}
Err(_) => {
// TODO: First commit the working copy
let stats = locked_wc
.check_out(&desired_wc_commit.tree())
.map_err(|err| {
CommandError::InternalError(format!(
"Failed to check out commit {}: {}",
desired_wc_commit.id().hex(),
err
))
})?;
locked_wc.finish(repo.op_id().clone());
ui.write("Working copy now at: ")?;
write_commit_summary(
ui.stdout_formatter().as_mut(),
repo.as_repo_ref(),
&workspace_id,
&desired_wc_commit,
ui.settings(),
)?;
ui.write("\n")?;
print_checkout_stats(ui, stats)?;
}
}
Ok(())
}
fn cmd_sparse(ui: &mut Ui, command: &CommandHelper, args: &SparseArgs) -> Result<(), CommandError> {
if args.list {
let workspace_command = command.workspace_helper(ui)?;

View File

@ -14,8 +14,6 @@
use std::path::Path;
use itertools::Itertools;
use crate::common::TestEnvironment;
pub mod common;
@ -96,7 +94,7 @@ fn test_workspaces_conflicting_edits() {
// Make changes in both working copies
std::fs::write(main_path.join("file"), "changed in main\n").unwrap();
std::fs::write(secondary_path.join("file"), "changed in second\n").unwrap();
// Squash the changes from the main workspace in the initial commit (before
// Squash the changes from the main workspace into the initial commit (before
// running any command in the secondary workspace
let stdout = test_env.jj_cmd_success(&main_path, &["squash"]);
insta::assert_snapshot!(stdout, @r###"
@ -104,7 +102,7 @@ fn test_workspaces_conflicting_edits() {
Working copy now at: fe8f41ed01d6 (no description set)
"###);
// The secondary workspace's checkout was updated
// The secondary workspace's working-copy commit was updated
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
@ fe8f41ed01d693b2d4365cd89e42ad9c531a939b default@
| o a1896a17282f19089a5cec44358d6609910e0513 secondary@
@ -112,19 +110,33 @@ fn test_workspaces_conflicting_edits() {
o c0d4a99ef98ada7da8dc73a778bbb747c4178385
o 0000000000000000000000000000000000000000
"###);
let stdout = get_log_output(&test_env, &secondary_path);
let stderr = test_env.jj_cmd_failure(&secondary_path, &["st"]);
insta::assert_snapshot!(stderr, @r###"
Error: The working copy is stale (not updated since operation 6a2e94fc65fb).
Hint: Run `jj workspace update-stale` to update it.
"###);
// Same error on second run, and from another command
let stderr = test_env.jj_cmd_failure(&secondary_path, &["log"]);
insta::assert_snapshot!(stderr, @r###"
Error: The working copy is stale (not updated since operation 6a2e94fc65fb).
Hint: Run `jj workspace update-stale` to update it.
"###);
let stdout = test_env.jj_cmd_success(&secondary_path, &["workspace", "update-stale"]);
// It was detected that the working copy is now stale
// TODO: Since there was an uncommitted change in the working copy, it should
// have been committed first (causing divergence)
assert!(stdout.starts_with("The working copy is stale"));
insta::assert_snapshot!(stdout.lines().skip(1).join("\n"), @r###"
insta::assert_snapshot!(stdout, @r###"
Working copy now at: a1896a17282f (no description set)
Added 0 files, modified 1 files, removed 0 files
"###);
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path),
@r###"
o fe8f41ed01d693b2d4365cd89e42ad9c531a939b default@
| @ a1896a17282f19089a5cec44358d6609910e0513 secondary@
|/
o c0d4a99ef98ada7da8dc73a778bbb747c4178385
o 0000000000000000000000000000000000000000
"###);
// The stale working copy should have been resolved by the previous command
let stdout = get_log_output(&test_env, &secondary_path);
assert!(!stdout.starts_with("The working copy is stale"));