mirror of
https://github.com/martinvonz/jj.git
synced 2024-09-20 02:07:15 +03:00
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:
parent
e1be0f5096
commit
a0573b1737
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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)?;
|
||||
|
@ -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"));
|
||||
|
Loading…
Reference in New Issue
Block a user