mod queries;
mod session;
use anyhow::Result;
use futures_util::TryStreamExt;
use jj_lib::{
backend::TreeValue, commit::Commit, ref_name::WorkspaceName, repo::Repo as _,
repo_path::RepoPath, revset::RevsetStreamExt,
};
use pollster::FutureExt as _;
use std::{
fs::{self, File},
path::PathBuf,
sync::Arc,
};
use tempfile::{TempDir, tempdir};
use zip::ZipArchive;
use crate::{
messages::{ChangeId, CommitId, RevId, RevSet, queries::RevsResult},
worker::{EventSink, WorkerSession, WorkspaceSession, queries::query_revisions},
};
pub struct NoProgress;
impl EventSink for NoProgress {
fn send(&self, _event_name: &str, _payload: serde_json::Value) {}
}
impl Default for WorkerSession {
fn default() -> Self {
WorkerSession {
force_log_page_size: None,
latest_query: None,
working_directory: None,
user_settings: crate::config::tests::settings_with_gg_defaults(),
sink: Arc::new(NoProgress),
ignore_immutable: false,
enable_askpass: false,
}
}
}
pub fn mkrepo() -> TempDir {
let repo_dir = tempdir().unwrap();
let mut archive_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
archive_path.push("res/test-repo.zip");
let archive_file = File::open(&archive_path).unwrap();
let mut archive = ZipArchive::new(archive_file).unwrap();
archive.extract(repo_dir.path()).unwrap();
repo_dir
}
pub fn mkid(xid: &str, cid: &str) -> RevId {
RevId {
change: ChangeId {
hex: xid.to_owned(),
prefix: xid.to_owned(),
rest: "".to_owned(),
offset: None,
is_divergent: false,
},
commit: CommitId {
hex: cid.to_owned(),
prefix: cid.to_owned(),
rest: "".to_owned(),
},
}
}
pub fn get_by_chid(ws: &WorkspaceSession, rev_id: &RevId) -> Result<Commit> {
use jj_lib::repo::Repo;
let revset = ws.evaluate_revset_str(&rev_id.change.hex)?;
let store = ws.repo().store();
let mut stream = revset.as_ref().stream().commits(store);
match stream.try_next().block_on()? {
Some(commit) => Ok(commit),
None => anyhow::bail!("Change {} not found", rev_id.change.hex),
}
}
pub async fn query_by_chid(ws: &WorkspaceSession<'_>, change_hex: &str) -> Result<RevsResult> {
let revset = ws.evaluate_revset_str(change_hex)?;
let store = ws.repo().store();
let commits: Vec<_> = revset.stream().commits(store).try_collect().await?;
let commit = commits
.first()
.ok_or_else(|| anyhow::anyhow!("not found"))?;
let id = ws.format_id(commit);
query_by_id(ws, id).await
}
pub async fn query_by_id(
ws: &crate::worker::gui_util::WorkspaceSession<'_>,
id: RevId,
) -> Result<RevsResult> {
query_revisions(ws, RevSet::singleton(id)).await
}
pub mod revs {
use crate::messages::RevId;
use super::mkid;
pub fn working_copy() -> RevId {
mkid("kvptxrkr", "e7080cd830960125c13e276aa056c811e7ce600a")
}
pub fn main_bookmark() -> RevId {
mkid("wnpusytq", "025843422c8f5374a4160fe79195b92d6ec3c6ee")
}
pub fn immutable_bookmark() -> RevId {
mkid("ywknyuol", "f86298e8166104062708cde7c1cf697022b4cf8b")
}
pub fn immutable_parent() -> RevId {
mkid("nxxylmpu", "fa32b17fcc7f44f176539feec6c13af413924329")
}
pub fn immutable_grandparent() -> RevId {
mkid("tqnnuvwv", "983d594962e861aa155c8cee9e49122978cec40f")
}
pub fn conflict_bookmark() -> RevId {
mkid("pkullrwy", "18edcaea9423cd9975c3f1ffbf07e00fe3ecc47a")
}
pub fn resolve_conflict() -> RevId {
mkid("yvtwywll", "461b914dbab3347a7c789bac200f0e135d03807e")
}
pub fn inherited_conflict() -> RevId {
mkid("tlxnptkw", "7241ca5bfef9f77eccb9544f8a69c61025d766c1")
}
pub fn chain_conflict() -> RevId {
mkid("vwxxopnk", "f80d4defdcf8627e7e8dca52fefb250e2e05d133")
}
pub fn chain_resolved() -> RevId {
mkid("lwzoqltx", "8c812d8bacb3ccb4ce4a3eff30e1221eef3373ca")
}
pub fn hunk_source() -> RevId {
mkid("xoooutru", "1b3949ce69432a74966165308ac30f5501fd9a83")
}
pub fn hunk_base() -> RevId {
mkid("xrqnzmzy", "71627400c7459f17fa45ea5dfd2572830f5c26ab")
}
pub fn hunk_child_single() -> RevId {
mkid("rwpmyumq", "cb56950fd81e14bcf30ea657f3c69a99ca743229")
}
pub fn hunk_child_multi() -> RevId {
mkid("nwywsplo", "b234894cba9641611cbd3e0648dd2ac3c634c272")
}
pub fn hunk_sibling() -> RevId {
mkid("lpvoqxrx", "489cf8d28d84c3477c65f89a856ba70ac91081bb")
}
pub fn hunk_grandchild() -> RevId {
mkid("onsonsrz", "1c073dfca738cdca246a1f8818f8f67bb3b4c8e6")
}
pub fn small_parent() -> RevId {
mkid("uqpmkpqu", "cd1a7fc72d71051f3a336a40da45d01d1d1a624c")
}
pub fn small_child() -> RevId {
mkid("vnstymnv", "f08d8a81983eb0c7849359b1555dca2d93016b54")
}
}
#[tokio::test]
async fn wc_path_is_visible() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let ws = session.load_workspace(repo.path()).await?;
let commit = ws.get_commit(ws.wc_id())?;
let value = commit
.tree()
.path_value(RepoPath::from_internal_string("a.txt")?)
.await?;
assert!(value.is_resolved());
assert!(
value
.first()
.as_ref()
.is_some_and(|x| matches!(x, TreeValue::File { .. }))
);
Ok(())
}
#[tokio::test]
async fn snapshot_updates_wc_if_changed() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let old_wc = ws.wc_id().clone();
assert!(!ws.import_and_snapshot(true, false).await?);
assert_eq!(&old_wc, ws.wc_id());
fs::write(repo.path().join("new.txt"), []).unwrap();
assert!(ws.import_and_snapshot(true, false).await?);
assert_ne!(&old_wc, ws.wc_id());
Ok(())
}
#[tokio::test]
async fn transaction_updates_wc_if_snapshot() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let old_wc = ws.wc_id().clone();
fs::write(repo.path().join("new.txt"), []).unwrap();
let tx = ws.start_transaction().await?;
ws.finish_transaction(tx, "do nothing").await?;
assert_ne!(&old_wc, ws.wc_id());
Ok(())
}
#[tokio::test]
async fn transaction_snapshot_path_is_visible() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
fs::write(repo.path().join("new.txt"), []).unwrap();
let tx = ws.start_transaction().await?;
ws.finish_transaction(tx, "do nothing").await?;
let commit = ws.get_commit(ws.wc_id())?;
let value = commit
.tree()
.path_value(RepoPath::from_internal_string("new.txt")?)
.await?;
assert!(value.is_resolved());
assert!(
value
.first()
.as_ref()
.is_some_and(|x| matches!(x, TreeValue::File { .. }))
);
Ok(())
}
static XDG_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[tokio::test]
async fn snapshot_respects_xdg_gitignore_colocated() -> Result<()> {
let _lock = XDG_ENV_LOCK.lock().unwrap();
let xdg_dir = tempdir()?;
let ignore_dir = xdg_dir.path().join("git");
fs::create_dir_all(&ignore_dir)?;
fs::write(ignore_dir.join("ignore"), "*.ignored\n")?;
unsafe { std::env::set_var("XDG_CONFIG_HOME", xdg_dir.path()) };
let _guard = SetVarGuard("XDG_CONFIG_HOME");
let workspace_dir = tempdir()?;
let mut session = WorkerSession::default();
session
.init_repository(&workspace_dir.path().to_owned(), true)
.await?;
let mut ws = session.load_workspace(workspace_dir.path()).await?;
fs::write(workspace_dir.path().join("tracked.txt"), "hello")?;
fs::write(workspace_dir.path().join("should_be.ignored"), "hidden")?;
assert!(ws.import_and_snapshot(true, false).await?);
let commit = ws.get_commit(ws.wc_id())?;
let tracked = commit
.tree()
.path_value(RepoPath::from_internal_string("tracked.txt")?)
.await?;
let ignored = commit
.tree()
.path_value(RepoPath::from_internal_string("should_be.ignored")?)
.await?;
assert!(tracked.is_resolved() && tracked.first().as_ref().is_some());
assert!(ignored.is_absent());
Ok(())
}
#[tokio::test]
async fn snapshot_respects_xdg_gitignore_internal() -> Result<()> {
let _lock = XDG_ENV_LOCK.lock().unwrap();
let xdg_dir = tempdir()?;
let ignore_dir = xdg_dir.path().join("git");
fs::create_dir_all(&ignore_dir)?;
fs::write(ignore_dir.join("ignore"), "*.ignored\n")?;
unsafe { std::env::set_var("XDG_CONFIG_HOME", xdg_dir.path()) };
let _guard = SetVarGuard("XDG_CONFIG_HOME");
let workspace_dir = tempdir()?;
let mut session = WorkerSession::default();
session
.init_repository(&workspace_dir.path().to_owned(), false)
.await?;
let mut ws = session.load_workspace(workspace_dir.path()).await?;
fs::write(workspace_dir.path().join("tracked.txt"), "hello")?;
fs::write(workspace_dir.path().join("should_be.ignored"), "hidden")?;
assert!(ws.import_and_snapshot(true, false).await?);
let commit = ws.get_commit(ws.wc_id())?;
let tracked = commit
.tree()
.path_value(RepoPath::from_internal_string("tracked.txt")?)
.await?;
let ignored = commit
.tree()
.path_value(RepoPath::from_internal_string("should_be.ignored")?)
.await?;
assert!(tracked.is_resolved() && tracked.first().as_ref().is_some());
assert!(ignored.is_absent());
Ok(())
}
#[tokio::test]
async fn add_workspace_creates_new_workspace() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let original_wc = ws.wc_id().clone();
let new_ws_path = repo.path().join("second-workspace");
ws.add_workspace("second".to_owned(), new_ws_path.clone())
.await?;
assert!(
ws.view()
.get_wc_commit_id(WorkspaceName::new("second"))
.is_some()
);
assert!(new_ws_path.join(".jj").exists());
assert_eq!(&original_wc, ws.wc_id());
let new_wc_id = ws
.view()
.get_wc_commit_id(WorkspaceName::new("second"))
.unwrap();
assert_ne!(new_wc_id, ws.wc_id());
let original_parents = ws.get_commit(ws.wc_id())?.parents().await?;
let new_parents = ws.get_commit(new_wc_id)?.parents().await?;
assert_eq!(
original_parents.iter().map(|c| c.id()).collect::<Vec<_>>(),
new_parents.iter().map(|c| c.id()).collect::<Vec<_>>()
);
Ok(())
}
#[tokio::test]
async fn add_workspace_rejects_duplicate_name() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let new_ws_path = repo.path().join("second-workspace");
ws.add_workspace("second".to_owned(), new_ws_path).await?;
let err = ws
.add_workspace("second".to_owned(), repo.path().join("third"))
.await
.unwrap_err();
assert!(
err.to_string().contains("already exists"),
"unexpected error: {err}"
);
Ok(())
}
#[tokio::test]
async fn add_workspace_rejects_empty_name() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let err = ws
.add_workspace("".to_owned(), repo.path().join("empty-name"))
.await
.unwrap_err();
assert!(
err.to_string().contains("cannot be empty"),
"unexpected error: {err}"
);
Ok(())
}
#[tokio::test]
async fn add_workspace_rejects_nonempty_destination() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let nonempty = repo.path().join("nonempty");
fs::create_dir(&nonempty)?;
fs::write(nonempty.join("file.txt"), "content")?;
let err = ws
.add_workspace("second".to_owned(), nonempty)
.await
.unwrap_err();
assert!(
err.to_string().contains("not an empty directory"),
"unexpected error: {err}"
);
Ok(())
}
#[tokio::test]
async fn forget_workspace_removes_workspace() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let new_ws_path = repo.path().join("to-forget");
ws.add_workspace("to-forget".to_owned(), new_ws_path.clone())
.await?;
assert!(
ws.view()
.get_wc_commit_id(WorkspaceName::new("to-forget"))
.is_some()
);
ws.forget_workspace("to-forget".to_owned()).await?;
assert!(
ws.view()
.get_wc_commit_id(WorkspaceName::new("to-forget"))
.is_none()
);
assert!(new_ws_path.exists());
Ok(())
}
#[tokio::test]
async fn forget_workspace_rejects_current() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let current_name = ws.name().as_str().to_owned();
let err = ws.forget_workspace(current_name).await.unwrap_err();
assert!(
err.to_string().contains("cannot forget the current"),
"unexpected error: {err}"
);
Ok(())
}
#[tokio::test]
async fn forget_workspace_rejects_nonexistent() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let err = ws
.forget_workspace("nonexistent".to_owned())
.await
.unwrap_err();
assert!(
err.to_string().contains("not found"),
"unexpected error: {err}"
);
Ok(())
}
#[tokio::test]
async fn list_workspaces_returns_sorted_names() -> Result<()> {
let repo = mkrepo();
let mut session = WorkerSession::default();
let mut ws = session.load_workspace(repo.path()).await?;
let current_name = ws.name().as_symbol().to_string();
assert_eq!(ws.list_workspaces(), vec![current_name.clone()]);
ws.add_workspace("alpha".to_owned(), repo.path().join("alpha"))
.await?;
ws.add_workspace("zeta".to_owned(), repo.path().join("zeta"))
.await?;
assert_eq!(
ws.list_workspaces(),
vec!["alpha".to_owned(), current_name, "zeta".to_owned()]
);
Ok(())
}
struct SetVarGuard(&'static str);
impl Drop for SetVarGuard {
fn drop(&mut self) {
unsafe { std::env::remove_var(self.0) };
}
}