feat: implement --new flag

The new `--new` or `-n` flag allows to create a new revision after the
single revision being described. Running `jj-cz --new` is the
equivalent of running `jj-cz @ && jj new`. Running `jj-cz --new xs` is
the equivalent of running `jj-cz xs && jj new xs`.

Passing several revisions to `jj-cz` with the `--new` flag will result
in an error.

Refs: #6
This commit is contained in:
2026-06-07 12:37:18 +02:00
parent 0e6b559d00
commit e45130d31b
9 changed files with 446 additions and 19 deletions
+88 -5
View File
@@ -11,18 +11,22 @@ use std::sync::{Mutex, atomic::AtomicBool};
/// Mock implementation of JjExecutor for testing
#[derive(Debug)]
pub struct MockJjExecutor {
/// Response to return from is_repository()
/// Response to return from `is_repository()`
is_repo_response: Result<bool, Error>,
/// Response to return from describe()
/// Response to return from `describe()`
describe_response: Result<(), Error>,
/// Track described revsets
described_revsets: Mutex<Vec<String>>,
/// Track response to return from get_description()
/// Track response to return from `get_description()`
get_description_response: Result<String, Error>,
/// Track calls to is_repository()
/// Track calls to `is_repository()`
is_repo_called: AtomicBool,
/// Track calls to describe() with the message passed
/// Track calls to `describe()` with the message passed
describe_calls: Mutex<Vec<String>>,
/// Track response to return from `new_revision()`
new_revision_response: Result<(), Error>,
/// Track calls to `new_revision()`
new_revision_calls: Mutex<Vec<String>>,
}
impl Default for MockJjExecutor {
@@ -34,6 +38,8 @@ impl Default for MockJjExecutor {
get_description_response: Ok(String::new()),
is_repo_called: AtomicBool::new(false),
describe_calls: Mutex::new(Vec::new()),
new_revision_response: Ok(()),
new_revision_calls: Mutex::new(Vec::new()),
}
}
}
@@ -66,6 +72,15 @@ impl MockJjExecutor {
pub fn describe_messages(&self) -> Vec<String> {
self.describe_calls.lock().unwrap().clone()
}
pub fn with_new_revision_response(mut self, response: Result<(), Error>) -> Self {
self.new_revision_response = response;
self
}
pub fn new_revision_calls(&self) -> Vec<String> {
self.new_revision_calls.lock().unwrap().clone()
}
}
#[async_trait(?Send)]
@@ -97,6 +112,17 @@ impl JjExecutor for MockJjExecutor {
async fn get_description(&self, _revset: &str) -> Result<String, Error> {
self.get_description_response.clone()
}
async fn new_revision(&self, revset: &str) -> Result<(), Error> {
self.new_revision_calls
.lock()
.unwrap()
.push(revset.to_string());
match &self.new_revision_response {
Ok(()) => Ok(()),
Err(e) => Err(e.clone()),
}
}
}
#[cfg(test)]
@@ -245,4 +271,61 @@ mod tests {
mock.is_repository().await.unwrap();
assert!(mock.was_is_repo_called());
}
/// Test mock new_revision() records the revset
#[tokio::test]
async fn mock_new_revision_records_revset() {
let mock = MockJjExecutor::new();
let result = mock.new_revision("@").await;
assert!(result.is_ok());
let calls = mock.new_revision_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0], "@");
}
/// Test mock new_revision() records multiple calls
#[tokio::test]
async fn mock_new_revision_records_multiple_calls() {
let mock = MockJjExecutor::new();
mock.new_revision("@").await.unwrap();
mock.new_revision("abc").await.unwrap();
mock.new_revision("xyz").await.unwrap();
let calls = mock.new_revision_calls();
assert_eq!(calls.len(), 3);
assert_eq!(calls[0], "@");
assert_eq!(calls[1], "abc");
assert_eq!(calls[2], "xyz");
}
/// Test mock new_revision() returns configured error
#[tokio::test]
async fn mock_new_revision_returns_error() {
let mock = MockJjExecutor::new().with_new_revision_response(Err(Error::RepositoryLocked));
let result = mock.new_revision("@").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::RepositoryLocked));
}
/// Test mock new_revision() records revset even on error
#[tokio::test]
async fn mock_new_revision_records_revset_on_error() {
let mock = MockJjExecutor::new().with_new_revision_response(Err(Error::JjOperation {
context: "failed".to_string(),
}));
let result = mock.new_revision("abc").await;
assert!(result.is_err());
let calls = mock.new_revision_calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0], "abc");
}
/// Test mock new_revision() can be inspected after success
#[tokio::test]
async fn mock_new_revision_returns_ok_and_tracks_revset() {
let mock = MockJjExecutor::new();
let result = mock.new_revision("my-feature").await;
assert!(result.is_ok());
let calls = mock.new_revision_calls();
assert_eq!(calls, vec!["my-feature"]);
}
}