Skip to content

Commit

Permalink
cli: Support renaming workspaces
Browse files Browse the repository at this point in the history
fixes #4342
  • Loading branch information
kevincliao committed Sep 17, 2024
1 parent 5bd7588 commit 412ef36
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
(inherit from parent; default), `full` (full working copy), or `empty` (the
empty working copy).

* New command `jj workspace rename` that can rename the current workspace.

* `jj op log` gained an option to include operation diffs.

* `jj git clone` now accepts a `--remote <REMOTE NAME>` option, which
Expand Down
4 changes: 4 additions & 0 deletions cli/examples/custom-working-copy/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ impl LockedWorkingCopy for LockedConflictsWorkingCopy {
self.inner.check_out(commit)
}

fn rename_workspace(&mut self, new_workspace_id: WorkspaceId) {
self.inner.rename_workspace(new_workspace_id);
}

fn reset(&mut self, commit: &Commit) -> Result<(), ResetError> {
self.inner.reset(commit)
}
Expand Down
7 changes: 7 additions & 0 deletions cli/src/command_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ use jj_lib::revset::RevsetParseErrorKind;
use jj_lib::revset::RevsetResolutionError;
use jj_lib::signing::SignInitError;
use jj_lib::str_util::StringPatternParseError;
use jj_lib::view::RenameWorkspaceError;
use jj_lib::working_copy::ResetError;
use jj_lib::working_copy::SnapshotError;
use jj_lib::working_copy::WorkingCopyStateError;
Expand Down Expand Up @@ -264,6 +265,12 @@ impl From<CheckOutCommitError> for CommandError {
}
}

impl From<RenameWorkspaceError> for CommandError {
fn from(err: RenameWorkspaceError) -> Self {
user_error_with_message("Failed to rename a workspace", err)
}
}

impl From<BackendError> for CommandError {
fn from(err: BackendError) -> Self {
match &err {
Expand Down
5 changes: 5 additions & 0 deletions cli/src/commands/workspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
mod add;
mod forget;
mod list;
mod rename;
mod root;
mod update_stale;

Expand All @@ -27,6 +28,8 @@ use self::forget::cmd_workspace_forget;
use self::forget::WorkspaceForgetArgs;
use self::list::cmd_workspace_list;
use self::list::WorkspaceListArgs;
use self::rename::cmd_workspace_rename;
use self::rename::WorkspaceRenameArgs;
use self::root::cmd_workspace_root;
use self::root::WorkspaceRootArgs;
use self::update_stale::cmd_workspace_update_stale;
Expand All @@ -51,6 +54,7 @@ pub(crate) enum WorkspaceCommand {
Add(WorkspaceAddArgs),
Forget(WorkspaceForgetArgs),
List(WorkspaceListArgs),
Rename(WorkspaceRenameArgs),
Root(WorkspaceRootArgs),
UpdateStale(WorkspaceUpdateStaleArgs),
}
Expand All @@ -65,6 +69,7 @@ pub(crate) fn cmd_workspace(
WorkspaceCommand::Add(args) => cmd_workspace_add(ui, command, args),
WorkspaceCommand::Forget(args) => cmd_workspace_forget(ui, command, args),
WorkspaceCommand::List(args) => cmd_workspace_list(ui, command, args),
WorkspaceCommand::Rename(args) => cmd_workspace_rename(ui, command, args),
WorkspaceCommand::Root(args) => cmd_workspace_root(ui, command, args),
WorkspaceCommand::UpdateStale(args) => cmd_workspace_update_stale(ui, command, args),
}
Expand Down
78 changes: 78 additions & 0 deletions cli/src/commands/workspace/rename.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2020 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use jj_lib::op_store::WorkspaceId;
use tracing::instrument;

use crate::cli_util::CommandHelper;
use crate::command_error::user_error;
use crate::command_error::CommandError;
use crate::ui::Ui;

/// Renames the current workspace
#[derive(clap::Args, Clone, Debug)]
pub struct WorkspaceRenameArgs {
/// The name of the workspace to update to.
new_workspace_name: String,
}

#[instrument(skip_all)]
pub fn cmd_workspace_rename(
ui: &mut Ui,
command: &CommandHelper,
args: &WorkspaceRenameArgs,
) -> Result<(), CommandError> {
if args.new_workspace_name.is_empty() {
return Err(user_error("New workspace name cannot be empty"));
}

let mut workspace_command = command.workspace_helper(ui)?;

let old_workspace_id = workspace_command.working_copy().workspace_id().clone();
let new_workspace_id = WorkspaceId::new(args.new_workspace_name.clone());
if new_workspace_id == old_workspace_id {
writeln!(ui.status(), "Nothing changed.")?;
return Ok(());
}

if workspace_command
.repo()
.view()
.get_wc_commit_id(&old_workspace_id)
.is_none()
{
return Err(user_error(format!(
"The current workspace '{}' is not tracked in the repo.",
old_workspace_id.as_str()
)));
}

let mut tx = workspace_command.start_transaction().into_inner();
let (mut locked_ws, _wc_commit) = workspace_command.start_working_copy_mutation()?;

locked_ws
.locked_wc()
.rename_workspace(new_workspace_id.clone());

tx.repo_mut()
.rename_workspace(&old_workspace_id, new_workspace_id)?;
let repo = tx.commit(format!(
"Renamed workspace '{}' to '{}'",
old_workspace_id.as_str(),
args.new_workspace_name
));
locked_ws.finish(repo.op_id().clone())?;

Ok(())
}
14 changes: 14 additions & 0 deletions cli/tests/cli-reference@.md.snap
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ This document contains the help content for the `jj` command-line program.
* [`jj workspace add`↴](#jj-workspace-add)
* [`jj workspace forget`↴](#jj-workspace-forget)
* [`jj workspace list`↴](#jj-workspace-list)
* [`jj workspace rename`↴](#jj-workspace-rename)
* [`jj workspace root`↴](#jj-workspace-root)
* [`jj workspace update-stale`↴](#jj-workspace-update-stale)

Expand Down Expand Up @@ -2144,6 +2145,7 @@ Each workspace also has own sparse patterns.
* `add` — Add a workspace
* `forget` — Stop tracking a workspace's working-copy commit in the repo
* `list` — List workspaces
* `rename` — Renames the current workspace
* `root` — Show the current workspace root directory
* `update-stale` — Update a workspace that has become stale
Expand Down Expand Up @@ -2208,6 +2210,18 @@ List workspaces
## `jj workspace rename`
Renames the current workspace
**Usage:** `jj workspace rename <NEW_WORKSPACE_NAME>`
###### **Arguments:**
* `<NEW_WORKSPACE_NAME>` — The name of the workspace to update to
## `jj workspace root`
Show the current workspace root directory
Expand Down
87 changes: 87 additions & 0 deletions cli/tests/test_workspaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,93 @@ fn test_debug_snapshot() {
"###);
}

#[test]
fn test_workspaces_rename_nothing_changed() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
let (stdout, stderr) = test_env.jj_cmd_ok(&main_path, &["workspace", "rename", "default"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Nothing changed.
"###);
}

#[test]
fn test_workspaces_rename_new_workspace_name_already_used() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
let stderr = test_env.jj_cmd_failure(&main_path, &["workspace", "rename", "second"]);
insta::assert_snapshot!(stderr, @r###"
Error: Failed to rename a workspace
Caused by: Workspace second already exists
"###);
}

#[test]
fn test_workspaces_rename_forgotten_workspace() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
test_env.jj_cmd_ok(&main_path, &["workspace", "forget", "second"]);
let secondary_path = test_env.env_root().join("secondary");
let stderr = test_env.jj_cmd_failure(&secondary_path, &["workspace", "rename", "third"]);
insta::assert_snapshot!(stderr, @r###"
Error: The current workspace 'second' is not tracked in the repo.
"###);
}

#[test]
fn test_workspaces_rename_workspace() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "main"]);
let main_path = test_env.env_root().join("main");
test_env.jj_cmd_ok(
&main_path,
&["workspace", "add", "--name", "second", "../secondary"],
);
let secondary_path = test_env.env_root().join("secondary");

// Both workspaces show up when we list them
let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: qpvuntsm 230dd059 (empty) (no description set)
second: uuqppmxq 57d63245 (empty) (no description set)
"###);

let stdout = test_env.jj_cmd_success(&secondary_path, &["workspace", "rename", "third"]);
insta::assert_snapshot!(stdout, @"");

let stdout = test_env.jj_cmd_success(&main_path, &["workspace", "list"]);
insta::assert_snapshot!(stdout, @r###"
default: qpvuntsm 230dd059 (empty) (no description set)
third: uuqppmxq 57d63245 (empty) (no description set)
"###);

// Can see the working-copy commit in each workspace in the log output.
insta::assert_snapshot!(get_log_output(&test_env, &main_path), @r###"
○ 57d63245a308 third@
│ @ 230dd059e1b0 default@
├─╯
◆ 000000000000
"###);
insta::assert_snapshot!(get_log_output(&test_env, &secondary_path), @r###"
@ 57d63245a308 third@
│ ○ 230dd059e1b0 default@
├─╯
◆ 000000000000
"###);
}

fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String {
let template = r#"
separate(" ",
Expand Down
11 changes: 10 additions & 1 deletion lib/src/local_working_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,7 @@ impl WorkingCopy for LocalWorkingCopy {
old_operation_id,
old_tree_id,
tree_state_dirty: false,
new_workspace_id: None,
}))
}
}
Expand Down Expand Up @@ -1825,6 +1826,7 @@ pub struct LockedLocalWorkingCopy {
old_operation_id: OperationId,
old_tree_id: MergedTreeId,
tree_state_dirty: bool,
new_workspace_id: Option<WorkspaceId>,
}

impl LockedWorkingCopy for LockedLocalWorkingCopy {
Expand Down Expand Up @@ -1872,6 +1874,10 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy {
Ok(stats)
}

fn rename_workspace(&mut self, new_workspace_id: WorkspaceId) {
self.new_workspace_id = Some(new_workspace_id);
}

fn reset(&mut self, commit: &Commit) -> Result<(), ResetError> {
let new_tree = commit.tree()?;
self.wc
Expand Down Expand Up @@ -1937,7 +1943,10 @@ impl LockedWorkingCopy for LockedLocalWorkingCopy {
err: Box::new(err),
})?;
}
if self.old_operation_id != operation_id {
if self.old_operation_id != operation_id || self.new_workspace_id.is_some() {
if let Some(new_workspace_id) = self.new_workspace_id {
self.wc.checkout_state_mut().workspace_id = new_workspace_id;
}
self.wc.checkout_state_mut().operation_id = operation_id;
self.wc.save();
}
Expand Down
10 changes: 10 additions & 0 deletions lib/src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ use crate::simple_op_store::SimpleOpStore;
use crate::store::Store;
use crate::submodule_store::SubmoduleStore;
use crate::transaction::Transaction;
use crate::view::RenameWorkspaceError;
use crate::view::View;

pub trait Repo {
Expand Down Expand Up @@ -1353,6 +1354,15 @@ impl MutableRepo {
Ok(())
}

pub fn rename_workspace(
&mut self,
old_workspace_id: &WorkspaceId,
new_workspace_id: WorkspaceId,
) -> Result<(), RenameWorkspaceError> {
self.view_mut()
.rename_workspace(old_workspace_id, new_workspace_id)
}

pub fn check_out(
&mut self,
workspace_id: WorkspaceId,
Expand Down
34 changes: 34 additions & 0 deletions lib/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::collections::HashMap;
use std::collections::HashSet;

use itertools::Itertools;
use thiserror::Error;

use crate::backend::CommitId;
use crate::op_store;
Expand Down Expand Up @@ -95,6 +96,29 @@ impl View {
self.data.wc_commit_ids.remove(workspace_id);
}

pub fn rename_workspace(
&mut self,
old_workspace_id: &WorkspaceId,
new_workspace_id: WorkspaceId,
) -> Result<(), RenameWorkspaceError> {
if self.data.wc_commit_ids.contains_key(&new_workspace_id) {
return Err(RenameWorkspaceError::WorkspaceAlreadyExists {
workspace_id: new_workspace_id.as_str().to_owned(),
});
}
let wc_commit_id = self
.data
.wc_commit_ids
.remove(old_workspace_id)
.ok_or_else(|| RenameWorkspaceError::WorkspaceDoesNotExist {
workspace_id: old_workspace_id.as_str().to_owned(),
})?;
self.data
.wc_commit_ids
.insert(new_workspace_id, wc_commit_id);
Ok(())
}

pub fn add_head(&mut self, head_id: &CommitId) {
self.data.head_ids.insert(head_id.clone());
}
Expand Down Expand Up @@ -369,3 +393,13 @@ impl View {
&mut self.data
}
}

/// Error from attempts to rename a workspace
#[derive(Debug, Error)]
pub enum RenameWorkspaceError {
#[error("Workspace {workspace_id} not found")]
WorkspaceDoesNotExist { workspace_id: String },

#[error("Workspace {workspace_id} already exists")]
WorkspaceAlreadyExists { workspace_id: String },
}
Loading

0 comments on commit 412ef36

Please sign in to comment.