feat: complete JjLib describe implementation

This commit is contained in:
2026-03-07 00:37:43 +01:00
parent bd20747bae
commit 1b66d7f86c
5 changed files with 124 additions and 23 deletions

View File

@@ -8,4 +8,4 @@ mod description;
pub use description::{Description, DescriptionError}; pub use description::{Description, DescriptionError};
mod message; mod message;
pub use message::{CommitMessageError, ConventionalCommit}; pub use message::CommitMessageError;

View File

@@ -1,4 +1,4 @@
use lazy_regex::regex_find;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[repr(transparent)] #[repr(transparent)]

View File

@@ -56,3 +56,27 @@ impl From<jj_lib::config::ConfigGetError> for Error {
Self::FailedReadingConfig Self::FailedReadingConfig
} }
} }
impl From<jj_lib::repo::RepoLoaderError> for Error {
fn from(error: jj_lib::repo::RepoLoaderError) -> Self {
Self::JjOperation {
context: format!("Failed to load repository: {}", error),
}
}
}
impl From<jj_lib::backend::BackendError> for Error {
fn from(error: jj_lib::backend::BackendError) -> Self {
Self::JjOperation {
context: format!("Backend operation failed: {}", error),
}
}
}
impl From<jj_lib::transaction::TransactionCommitError> for Error {
fn from(error: jj_lib::transaction::TransactionCommitError) -> Self {
Self::JjOperation {
context: format!("Transaction commit failed: {}", error),
}
}
}

View File

@@ -1,9 +1,9 @@
use std::path::Path; use std::path::Path;
use jj_lib::config::StackedConfig; use jj_lib::config::StackedConfig;
use jj_lib::repo::{Repo, StoreFactories};
use jj_lib::settings::UserSettings; use jj_lib::settings::UserSettings;
use jj_lib::workspace::{Workspace, default_working_copy_factories}; use jj_lib::workspace::{Workspace, default_working_copy_factories};
use jj_lib::repo::{Repo, StoreFactories};
use crate::error::Error; use crate::error::Error;
use crate::jj::JjExecutor; use crate::jj::JjExecutor;
@@ -46,15 +46,82 @@ impl JjExecutor for JjLib {
let settings = UserSettings::from_config(config)?; let settings = UserSettings::from_config(config)?;
let store_factories = StoreFactories::default(); let store_factories = StoreFactories::default();
let wc_factories = default_working_copy_factories(); let wc_factories = default_working_copy_factories();
match Workspace::load(&settings, &std::env::current_dir()?, &store_factories, &wc_factories) {
Ok(_) => Ok(true), // Check if the directory exists first
Err(_) => Ok(false) if !self.working_dir.exists() {
return Ok(false);
} }
// Try to load workspace from the working directory
// Walk up the directory tree until we find a repository or reach the root
let mut current_dir = self.working_dir.clone();
loop {
match Workspace::load(&settings, &current_dir, &store_factories, &wc_factories) {
Ok(_) => return Ok(true),
Err(_) => {
// Move up to parent directory
if !current_dir.pop() {
// Reached root directory
break;
}
}
}
}
Ok(false)
} }
async fn describe(&self, _message: &str) -> Result<(), Error> { async fn describe(&self, message: &str) -> Result<(), Error> {
// TODO: T018/T019 - Implement using jj-lib transactions // Load the repository
todo!("T018/T019: Implement describe() using jj-lib") let config = StackedConfig::with_defaults();
let settings = UserSettings::from_config(config)?;
let store_factories = StoreFactories::default();
let wc_factories = default_working_copy_factories();
let workspace = Workspace::load(
&settings,
&self.working_dir,
&store_factories,
&wc_factories,
)
.map_err(|_| Error::NotARepository)?;
let repo = workspace.repo_loader().load_at_head()?;
// Start a transaction
let mut tx = repo.start_transaction();
tx.set_tag("args".to_string(), "jj-cz describe".to_string());
// Get the current working copy commit (equivalent to @ revset)
let view = tx.repo().view();
let wc_commit_ids = view.wc_commit_ids();
if wc_commit_ids.is_empty() {
return Err(Error::JjOperation {
context: "No working copy commit found".to_string(),
});
}
// Get the first working copy commit (usually there's only one)
let wc_commit_id = wc_commit_ids.values().next().unwrap();
let wc_commit = tx.repo().store().get_commit(wc_commit_id)?;
// Rewrite the working copy commit with the new description
let commit_builder = tx
.repo_mut()
.rewrite_commit(&wc_commit)
.set_description(message);
// Write the modified commit
let _new_commit = commit_builder.write()?;
// Rebase descendants after the rewrite
tx.repo_mut().rebase_descendants()?;
// Finish the transaction
tx.commit("jj-cz: update commit description")?;
Ok(())
} }
} }
@@ -72,8 +139,7 @@ mod tests {
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
return Err(std::io::Error::new( return Err(std::io::Error::other(
std::io::ErrorKind::Other,
format!( format!(
"jj git init failed: {}", "jj git init failed: {}",
String::from_utf8_lossy(&output.stderr) String::from_utf8_lossy(&output.stderr)
@@ -91,12 +157,8 @@ mod tests {
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
return Err(std::io::Error::new( return Err(std::io::Error::other(
std::io::ErrorKind::Other, format!("jj log failed: {}", String::from_utf8_lossy(&output.stderr)),
format!(
"jj log failed: {}",
String::from_utf8_lossy(&output.stderr)
),
)); ));
} }
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
@@ -219,7 +281,11 @@ mod tests {
let test_message = "feat(scope): add new feature"; let test_message = "feat(scope): add new feature";
let result = jj_lib.describe(test_message).await; let result = jj_lib.describe(test_message).await;
assert!(result.is_ok(), "describe() should succeed, got {:?}", result); assert!(
result.is_ok(),
"describe() should succeed, got {:?}",
result
);
// Verify the commit description was updated // Verify the commit description was updated
let actual_description = let actual_description =
@@ -239,10 +305,14 @@ mod tests {
let jj_lib = JjLib::with_working_dir(temp_dir.path()); let jj_lib = JjLib::with_working_dir(temp_dir.path());
let multiline_message = "feat: add feature\n\nThis is the body of the commit.\nIt spans multiple lines."; let multiline_message =
"feat: add feature\n\nThis is the body of the commit.\nIt spans multiple lines.";
let result = jj_lib.describe(multiline_message).await; let result = jj_lib.describe(multiline_message).await;
assert!(result.is_ok(), "describe() should succeed with multiline message"); assert!(
result.is_ok(),
"describe() should succeed with multiline message"
);
let actual_description = get_commit_description(temp_dir.path()).unwrap(); let actual_description = get_commit_description(temp_dir.path()).unwrap();
assert_eq!(actual_description, multiline_message); assert_eq!(actual_description, multiline_message);
@@ -264,7 +334,10 @@ mod tests {
// Then clear it with empty message // Then clear it with empty message
let result = jj_lib.describe("").await; let result = jj_lib.describe("").await;
assert!(result.is_ok(), "describe() should succeed with empty message"); assert!(
result.is_ok(),
"describe() should succeed with empty message"
);
let actual_description = get_commit_description(temp_dir.path()).unwrap(); let actual_description = get_commit_description(temp_dir.path()).unwrap();
assert_eq!(actual_description, "", "Description should be cleared"); assert_eq!(actual_description, "", "Description should be cleared");
@@ -281,7 +354,10 @@ mod tests {
let special_message = "fix: handle \"quotes\" and 'apostrophes' & <brackets>"; let special_message = "fix: handle \"quotes\" and 'apostrophes' & <brackets>";
let result = jj_lib.describe(special_message).await; let result = jj_lib.describe(special_message).await;
assert!(result.is_ok(), "describe() should handle special characters"); assert!(
result.is_ok(),
"describe() should handle special characters"
);
let actual_description = get_commit_description(temp_dir.path()).unwrap(); let actual_description = get_commit_description(temp_dir.path()).unwrap();
assert_eq!(actual_description, special_message); assert_eq!(actual_description, special_message);

View File

@@ -58,7 +58,8 @@ mod tests {
/// Check if is_repository() was called /// Check if is_repository() was called
fn was_is_repo_called(&self) -> bool { fn was_is_repo_called(&self) -> bool {
self.is_repo_called.load(std::sync::atomic::Ordering::SeqCst) self.is_repo_called
.load(std::sync::atomic::Ordering::SeqCst)
} }
/// Get all messages passed to describe() /// Get all messages passed to describe()