diff --git a/src/commit/types/mod.rs b/src/commit/types/mod.rs index 2002720..840d8b9 100644 --- a/src/commit/types/mod.rs +++ b/src/commit/types/mod.rs @@ -8,4 +8,4 @@ mod description; pub use description::{Description, DescriptionError}; mod message; -pub use message::{CommitMessageError, ConventionalCommit}; +pub use message::CommitMessageError; diff --git a/src/commit/types/scope.rs b/src/commit/types/scope.rs index e4f143b..7ebbef6 100644 --- a/src/commit/types/scope.rs +++ b/src/commit/types/scope.rs @@ -1,4 +1,4 @@ -use lazy_regex::regex_find; + #[derive(Debug, Clone, PartialEq, Eq)] #[repr(transparent)] diff --git a/src/error.rs b/src/error.rs index 76be22f..a82902b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -56,3 +56,27 @@ impl From for Error { Self::FailedReadingConfig } } + +impl From for Error { + fn from(error: jj_lib::repo::RepoLoaderError) -> Self { + Self::JjOperation { + context: format!("Failed to load repository: {}", error), + } + } +} + +impl From for Error { + fn from(error: jj_lib::backend::BackendError) -> Self { + Self::JjOperation { + context: format!("Backend operation failed: {}", error), + } + } +} + +impl From for Error { + fn from(error: jj_lib::transaction::TransactionCommitError) -> Self { + Self::JjOperation { + context: format!("Transaction commit failed: {}", error), + } + } +} diff --git a/src/jj/executor.rs b/src/jj/executor.rs index ab2f9f0..29ae474 100644 --- a/src/jj/executor.rs +++ b/src/jj/executor.rs @@ -1,9 +1,9 @@ use std::path::Path; use jj_lib::config::StackedConfig; +use jj_lib::repo::{Repo, StoreFactories}; use jj_lib::settings::UserSettings; use jj_lib::workspace::{Workspace, default_working_copy_factories}; -use jj_lib::repo::{Repo, StoreFactories}; use crate::error::Error; use crate::jj::JjExecutor; @@ -46,15 +46,82 @@ impl JjExecutor for JjLib { let settings = UserSettings::from_config(config)?; let store_factories = StoreFactories::default(); let wc_factories = default_working_copy_factories(); - match Workspace::load(&settings, &std::env::current_dir()?, &store_factories, &wc_factories) { - Ok(_) => Ok(true), - Err(_) => Ok(false) + + // Check if the directory exists first + 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, ¤t_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> { - // TODO: T018/T019 - Implement using jj-lib transactions - todo!("T018/T019: Implement describe() using jj-lib") + async fn describe(&self, message: &str) -> Result<(), Error> { + // Load the repository + 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()?; if !output.status.success() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, + return Err(std::io::Error::other( format!( "jj git init failed: {}", String::from_utf8_lossy(&output.stderr) @@ -91,12 +157,8 @@ mod tests { .output()?; if !output.status.success() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!( - "jj log failed: {}", - String::from_utf8_lossy(&output.stderr) - ), + return Err(std::io::Error::other( + format!("jj log failed: {}", String::from_utf8_lossy(&output.stderr)), )); } 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 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 let actual_description = @@ -239,10 +305,14 @@ mod tests { 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; - 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(); assert_eq!(actual_description, multiline_message); @@ -264,7 +334,10 @@ mod tests { // Then clear it with empty message 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(); assert_eq!(actual_description, "", "Description should be cleared"); @@ -281,7 +354,10 @@ mod tests { let special_message = "fix: handle \"quotes\" and 'apostrophes' & "; 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(); assert_eq!(actual_description, special_message); diff --git a/src/jj/mod.rs b/src/jj/mod.rs index f994eb2..3bed694 100644 --- a/src/jj/mod.rs +++ b/src/jj/mod.rs @@ -58,7 +58,8 @@ mod tests { /// Check if is_repository() was called 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()