use color_eyre::eyre::{ContextCompat, Result, WrapErr};
use jj_lib::{
backend::CommitId,
config::{ConfigLayer, ConfigSource, StackedConfig},
gitignore::GitIgnoreFile,
local_working_copy::{LocalWorkingCopy, LocalWorkingCopyFactory},
matchers::{EverythingMatcher, NothingMatcher},
op_store::RefTarget,
ref_name::RefNameBuf,
repo::{Repo, StoreFactories},
revset::{RevsetExpression, SymbolResolver, SymbolResolverExtension},
settings::UserSettings,
working_copy::SnapshotOptions,
workspace::{WorkingCopyFactories, Workspace},
};
use std::{env, path::PathBuf};
fn load_config() -> Result<StackedConfig> {
let mut config = StackedConfig::with_defaults();
if let Ok(jj_config) = env::var("JJ_CONFIG") {
for path in env::split_paths(&jj_config) {
if path.is_dir() {
config
.load_dir(ConfigSource::User, &path)
.wrap_err("Failed to load user config directory from $JJ_CONFIG")?;
} else if path.exists() {
config
.load_file(ConfigSource::User, path)
.wrap_err("Failed to load user config file from $JJ_CONFIG")?;
}
}
} else {
let xdg_base = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")));
if let Some(platform_config) = xdg_base.map(|d| d.join("jj").join("config.toml"))
&& platform_config.exists()
{
config
.load_file(ConfigSource::User, platform_config)
.wrap_err("Failed to load platform jj config")?;
}
if let Some(legacy_config) =
env::var_os("HOME").map(|h| PathBuf::from(h).join(".jjconfig.toml"))
&& legacy_config.exists()
{
config
.load_file(ConfigSource::User, legacy_config)
.wrap_err("Failed to load ~/.jjconfig.toml")?;
}
}
let mut overrides = ConfigLayer::empty(ConfigSource::EnvOverrides);
if let Ok(name) = env::var("JJ_USER") {
overrides
.set_value("user.name", name)
.wrap_err("Failed to set user.name from $JJ_USER")?;
}
if let Ok(email) = env::var("JJ_EMAIL") {
overrides
.set_value("user.email", email)
.wrap_err("Failed to set user.email from $JJ_EMAIL")?;
}
config.add_layer(overrides);
Ok(config)
}
fn load_workspace() -> Result<Workspace> {
let cwd = env::current_dir().wrap_err("Failed to get current directory")?;
let settings =
UserSettings::from_config(load_config()?).wrap_err("Failed to load jj settings")?;
let store_factories = StoreFactories::default();
let mut wc_factories = WorkingCopyFactories::default();
wc_factories.insert(
LocalWorkingCopy::name().to_owned(),
Box::new(LocalWorkingCopyFactory {}),
);
Workspace::load(&settings, &cwd, &store_factories, &wc_factories)
.wrap_err("Failed to load jj workspace")
}
pub async fn fetch_commit_messages(n: usize) -> Result<Vec<String>> {
let workspace = load_workspace()?;
let repo = workspace
.repo_loader()
.load_at_head()
.await
.wrap_err("Failed to load jj repo")?;
let workspace_name = workspace.workspace_name().to_owned();
let wc_expr = RevsetExpression::working_copy(workspace_name);
let ancestors_expr = wc_expr.ancestors_range(0..n as u64);
let extensions: Vec<Box<dyn SymbolResolverExtension>> = vec![];
let symbol_resolver = SymbolResolver::new(repo.as_ref(), &extensions);
let resolved = ancestors_expr
.resolve_user_expression(repo.as_ref(), &symbol_resolver)
.wrap_err("Failed to resolve revset expression")?;
let revset = resolved
.evaluate(repo.as_ref())
.wrap_err("Failed to evaluate revset")?;
let mut messages = Vec::new();
for commit_id in revset.iter() {
let commit_id = commit_id.wrap_err("Error iterating revset")?;
let commit = repo
.store()
.get_commit(&commit_id)
.wrap_err("Failed to get commit")?;
let desc = commit.description().trim().to_string();
if !desc.is_empty() {
messages.push(desc);
}
}
Ok(messages)
}
pub async fn commit(message: &str) -> Result<CommitId> {
let mut workspace = load_workspace()?;
let repo = workspace
.repo_loader()
.load_at_head()
.await
.wrap_err("Failed to load jj repo")?;
let workspace_name = workspace.workspace_name().to_owned();
let wc_commit_id = repo
.view()
.get_wc_commit_id(&workspace_name)
.cloned()
.wrap_err("No working-copy commit found for this workspace")?;
let wc_commit = repo
.store()
.get_commit(&wc_commit_id)
.wrap_err("Failed to get working-copy commit")?;
let root_gitignore = GitIgnoreFile::empty()
.chain_with_file("", workspace.workspace_root().join(".gitignore"))
.wrap_err("Failed to load .gitignore")?;
let mut locked_ws = workspace
.start_working_copy_mutation()
.wrap_err("Failed to lock working copy")?;
let snapshot_options = SnapshotOptions {
base_ignores: root_gitignore,
progress: None,
start_tracking_matcher: &EverythingMatcher,
force_tracking_matcher: &NothingMatcher,
max_new_file_size: u64::MAX,
};
let (snapshot_tree, _stats) = locked_ws
.locked_wc()
.snapshot(&snapshot_options)
.await
.wrap_err("Failed to snapshot working copy")?;
let mut tx = repo.start_transaction();
let repo = tx.repo_mut();
let new_commit = repo
.rewrite_commit(&wc_commit)
.set_tree(snapshot_tree)
.set_description(message)
.write()
.await
.wrap_err("Failed to write new commit")?;
repo.rebase_descendants()
.await
.wrap_err("Failed to rebase descendants")?;
let new_wc_commit = repo
.check_out(workspace_name.clone(), &new_commit)
.await
.wrap_err("Failed to check out new commit")?;
let new_repo = tx
.commit("commit")
.await
.wrap_err("Failed to commit transaction")?;
locked_ws
.locked_wc()
.check_out(&new_wc_commit)
.await
.wrap_err("Failed to update working copy to new commit")?;
locked_ws
.finish(new_repo.op_id().clone())
.wrap_err("Failed to finish working copy mutation")?;
Ok(new_commit.id().clone())
}
pub async fn find_nearest_ancestor_bookmarks() -> Result<Option<Vec<String>>> {
let workspace = load_workspace()?;
let repo = workspace
.repo_loader()
.load_at_head()
.await
.wrap_err("Failed to load jj repo")?;
let workspace_name = workspace.workspace_name().to_owned();
let wc_commit_id = repo
.view()
.get_wc_commit_id(&workspace_name)
.cloned()
.wrap_err("No working-copy commit found for this workspace")?;
let wc_commit = repo
.store()
.get_commit(&wc_commit_id)
.wrap_err("Failed to get working-copy commit")?;
let first_parent_id = match wc_commit.parent_ids().first() {
Some(id) => id.clone(),
None => return Ok(None),
};
let mut current = repo
.store()
.get_commit(&first_parent_id)
.wrap_err("Failed to get parent commit")?;
loop {
let names: Vec<String> = repo
.view()
.local_bookmarks_for_commit(current.id())
.map(|(name, _)| name.as_str().to_owned())
.collect();
if !names.is_empty() {
return Ok(Some(names));
}
let parent_ids = current.parent_ids();
if parent_ids.is_empty() {
return Ok(None);
}
current = repo
.store()
.get_commit(&parent_ids[0])
.wrap_err("Failed to get ancestor commit")?;
}
}
pub async fn advance_bookmark(name: &str, commit_id: &CommitId) -> Result<()> {
let mut workspace = load_workspace()?;
let repo = workspace
.repo_loader()
.load_at_head()
.await
.wrap_err("Failed to load jj repo")?;
let mut tx = repo.start_transaction();
let repo = tx.repo_mut();
let ref_name: RefNameBuf = name.into();
let target = RefTarget::normal(commit_id.clone());
repo.set_local_bookmark_target(&ref_name, target);
let new_repo = tx
.commit("advance bookmark")
.await
.wrap_err("Failed to commit bookmark transaction")?;
let workspace_name = workspace.workspace_name().to_owned();
let wc_commit_id = new_repo
.view()
.get_wc_commit_id(&workspace_name)
.cloned()
.wrap_err("No working-copy commit found after bookmark advance")?;
let wc_commit = new_repo
.store()
.get_commit(&wc_commit_id)
.wrap_err("Failed to get working-copy commit after bookmark advance")?;
workspace
.check_out(new_repo.op_id().clone(), None, &wc_commit)
.await
.wrap_err("Failed to update working copy after bookmark advance")?;
Ok(())
}