mod git_util;
mod gui_util;
mod mutations;
mod queries;
mod session;
#[cfg(all(test, not(feature = "ts-rs")))]
mod tests;
use std::env::{self, VarError};
use std::fmt::Debug;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Error, Result, anyhow};
use jj_cli::{git_util::load_git_import_options, ui::Ui};
use jj_lib::git::{self, GitFetch, GitFetchRefExpression, GitSettings};
use jj_lib::ref_name::{RefNameBuf, RemoteName, RemoteNameBuf, RemoteRefSymbol};
use jj_lib::repo::{Repo, StoreFactories};
use jj_lib::settings::UserSettings;
use jj_lib::str_util::StringExpression;
use jj_lib::workspace::{
self, DefaultWorkspaceLoaderFactory, Workspace, WorkspaceLoaderFactory as _,
};
use serde::Serialize;
use crate::messages::mutations::{MutationOptions, MutationResult};
use git_util::AuthContext;
pub use gui_util::WorkspaceSession;
pub use session::{Session, SessionEvent};
#[async_trait::async_trait(?Send)]
pub trait Mutation: Debug {
fn describe(&self) -> String {
std::any::type_name::<Self>().to_owned()
}
async fn execute(
self: Box<Self>,
ws: &mut WorkspaceSession,
options: &MutationOptions,
) -> Result<MutationResult>;
#[cfg(test)]
async fn execute_unboxed(self, ws: &mut WorkspaceSession) -> Result<MutationResult>
where
Self: Sized,
{
Box::new(self)
.execute(ws, &MutationOptions::default())
.await
}
}
pub trait EventSink: Send + Sync {
fn send(&self, event_name: &str, payload: serde_json::Value);
}
pub trait EventSinkExt: EventSink {
fn send_typed<T: Serialize>(&self, event_name: &str, payload: &T) {
let value = serde_json::to_value(payload).expect("T: Serialize");
self.send(event_name, value)
}
}
impl<S: EventSink + ?Sized> EventSinkExt for S {}
pub struct WorkerSession {
pub(crate) force_log_page_size: Option<usize>,
pub(crate) latest_query: Option<String>,
pub working_directory: Option<PathBuf>,
pub sink: Arc<dyn EventSink>,
pub user_settings: UserSettings,
pub ignore_immutable: bool,
pub enable_askpass: bool,
}
impl WorkerSession {
pub fn new(
sink: Arc<dyn EventSink>,
working_directory: Option<PathBuf>,
user_settings: UserSettings,
ignore_immutable: bool,
enable_askpass: bool,
) -> Self {
WorkerSession {
force_log_page_size: None,
latest_query: None,
sink,
working_directory,
user_settings,
ignore_immutable,
enable_askpass,
}
}
pub async fn init_repository(&self, location: &PathBuf, colocated: bool) -> Result<PathBuf> {
let jj_path = location.join(".jj");
if jj_path.exists() {
return Err(anyhow!(
"A Jujutsu repository already exists in this directory"
));
}
let canonical_location = dunce::canonicalize(location)?;
let (settings, _, _, _) = crate::config::read_config(None)?;
if colocated {
let git_path = location.join(".git");
if git_path.exists() {
Workspace::init_external_git(&settings, &canonical_location, &git_path).await?; } else {
Workspace::init_colocated_git(&settings, &canonical_location).await?; }
} else {
Workspace::init_internal_git(&settings, &canonical_location).await?; }
Ok(canonical_location)
}
pub async fn clone_repository(
&self,
source_url: &str,
location: &PathBuf,
colocated: bool,
) -> Result<PathBuf> {
if location.exists() {
let has_files = fs::read_dir(location)
.context("Failed to read destination directory")?
.next()
.is_some();
if has_files {
return Err(anyhow!(
"Destination directory is not empty: {}",
location.display()
));
}
} else {
fs::create_dir_all(location).context("Failed to create destination directory")?;
}
let canonical_location = dunce::canonicalize(location)?;
let (settings, _, _, _) = crate::config::read_config(None)?;
let (_workspace, repo) = if colocated {
Workspace::init_colocated_git(&settings, &canonical_location).await?
} else {
Workspace::init_internal_git(&settings, &canonical_location).await?
};
let remote_name = RemoteName::new("origin");
{
let mut tx = repo.start_transaction();
git::add_remote(
tx.repo_mut(),
remote_name,
source_url,
None, gix::remote::fetch::Tags::Included,
)
.context("add_remote(origin)")?;
tx.commit("add git remote origin").await?;
}
let loader = DefaultWorkspaceLoaderFactory.create(&canonical_location)?;
let workspace = loader.load(
&settings,
&StoreFactories::default(),
&workspace::default_working_copy_factories(),
)?;
let repo = workspace.repo_loader().load_at_head().await?;
let mut auth_ctx = AuthContext::new(None);
let git_settings = GitSettings::from_settings(&settings)?;
let remote_settings = settings.remote_settings()?;
let import_options = load_git_import_options(&Ui::null(), &git_settings, &remote_settings)
.map_err(|e| Error::new(e.error))?;
let fetch_result =
auth_ctx.with_callbacks(Some(self.sink.clone()), self.enable_askpass, |cb, env| {
let mut subprocess_options = git_settings.to_subprocess_options();
subprocess_options.environment = env;
let mut tx = repo.start_transaction();
let mut fetcher =
GitFetch::new(tx.repo_mut(), subprocess_options, &import_options)?;
let refspecs = git::expand_fetch_refspecs(
remote_name,
GitFetchRefExpression {
bookmark: StringExpression::all(),
tag: StringExpression::none(),
},
)?;
fetcher
.fetch(remote_name, refspecs, cb, None, None)
.context("Failed to fetch from remote")?;
pollster::block_on(fetcher.import_refs()).context("Failed to import refs")?;
let workspace_name = workspace.workspace_name().to_owned();
let mut default_branch = None;
if let Some(bookmark_name) = Self::find_default_branch(tx.repo()) {
let bookmark_ref = RefNameBuf::from(bookmark_name.as_str());
let remote_ref = RemoteNameBuf::from("origin");
let symbol = RemoteRefSymbol {
name: &bookmark_ref,
remote: &remote_ref,
};
let remote_bookmark = tx.repo().view().get_remote_bookmark(symbol);
if let Some(commit_id) = remote_bookmark.target.as_normal().cloned() {
let commit = tx.repo().store().get_commit(&commit_id)?;
tx.repo_mut().track_remote_bookmark(symbol)?;
default_branch = Some((workspace_name, commit, commit_id));
}
}
Ok::<_, Error>((tx, default_branch))
})?;
let (mut tx, default_branch) = fetch_result;
let mut checkout_commit_id = None;
if let Some((workspace_name, commit, commit_id)) = default_branch {
tx.repo_mut().check_out(workspace_name, &commit).await?;
checkout_commit_id = Some(commit_id);
}
tx.repo_mut().rebase_descendants().await?;
tx.commit("clone from git remote").await?;
if let Some(wc_id) = checkout_commit_id {
let loader = DefaultWorkspaceLoaderFactory.create(&canonical_location)?;
let mut workspace = loader.load(
&settings,
&jj_lib::repo::StoreFactories::default(),
&jj_lib::workspace::default_working_copy_factories(),
)?;
let repo = workspace.repo_loader().load_at_head().await?;
let commit = repo.store().get_commit(&wc_id)?;
workspace
.check_out(repo.op_id().clone(), None, &commit)
.await?;
}
Ok(canonical_location)
}
fn find_default_branch(repo: &dyn jj_lib::repo::Repo) -> Option<String> {
use jj_lib::ref_name::{RefNameBuf, RemoteNameBuf, RemoteRefSymbol};
for bookmark_name in ["main", "master", "trunk", "develop"] {
let bookmark_ref = RefNameBuf::from(bookmark_name);
let remote_ref = RemoteNameBuf::from("origin");
let symbol = RemoteRefSymbol {
name: &bookmark_ref,
remote: &remote_ref,
};
if repo.view().get_remote_bookmark(symbol).is_present() {
return Some(bookmark_name.to_string());
}
}
for (symbol, _) in repo.view().all_remote_bookmarks() {
if symbol.remote.as_str() == "origin" {
return Some(symbol.name.as_str().to_string());
}
}
None
}
fn get_cwd(&self) -> Result<PathBuf> {
self.working_directory
.as_ref()
.map(|cwd| Ok(dunce::canonicalize(cwd.clone())?))
.or_else(|| match env::var("OWD") {
Ok(var) => Some(Ok(PathBuf::from(var))),
Err(VarError::NotPresent) => None,
Err(err) => Some(Err(anyhow!(err))),
})
.unwrap_or_else(|| env::current_dir().map_err(Error::new))
}
}