1757 lines
57 KiB
Rust
1757 lines
57 KiB
Rust
#![cfg(unix)]
|
|
|
|
use std::{
|
|
collections::BTreeMap,
|
|
ffi::OsString,
|
|
fs,
|
|
os::unix::{
|
|
ffi::OsStringExt,
|
|
fs::{PermissionsExt, symlink},
|
|
},
|
|
path::{Path, PathBuf},
|
|
process::{Command, Output},
|
|
sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, AtomicU64, Ordering},
|
|
},
|
|
};
|
|
use vamp::{
|
|
diff::{Category, Comparison, ComparisonMode, FileStatus, RepoKind, Revision, RevisionTarget},
|
|
repository::Repository,
|
|
};
|
|
|
|
static NEXT: AtomicU64 = AtomicU64::new(0);
|
|
struct Fixture(PathBuf);
|
|
impl Fixture {
|
|
fn new() -> Self {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"vamp-repository-test-{}-{}",
|
|
std::process::id(),
|
|
NEXT.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
fs::create_dir(&path).unwrap();
|
|
Self(path)
|
|
}
|
|
fn dir(&self, name: &str) -> PathBuf {
|
|
let path = self.0.join(name);
|
|
fs::create_dir(&path).unwrap();
|
|
path
|
|
}
|
|
}
|
|
impl Drop for Fixture {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
fn command(program: &str, root: &Path, args: &[&str]) -> Output {
|
|
Command::new(program)
|
|
.current_dir(root)
|
|
.args(args)
|
|
.env("GIT_CONFIG_NOSYSTEM", "1")
|
|
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
|
.env("GIT_OPTIONAL_LOCKS", "0")
|
|
.env("GIT_TERMINAL_PROMPT", "0")
|
|
.env("GIT_AUTHOR_DATE", "2000-01-01T00:00:00Z")
|
|
.env("GIT_COMMITTER_DATE", "2000-01-01T00:00:00Z")
|
|
.env("JJ_CONFIG", "/dev/null")
|
|
.env("JJ_USER", "Diff Test")
|
|
.env("JJ_EMAIL", "diff@example.invalid")
|
|
.output()
|
|
.unwrap()
|
|
}
|
|
fn success(program: &str, root: &Path, args: &[&str]) -> String {
|
|
let result = command(program, root, args);
|
|
assert!(
|
|
result.status.success(),
|
|
"{program} {args:?}: {}",
|
|
String::from_utf8_lossy(&result.stderr)
|
|
);
|
|
String::from_utf8(result.stdout)
|
|
.unwrap()
|
|
.trim_end_matches('\n')
|
|
.into()
|
|
}
|
|
fn git(root: &Path, args: &[&str]) -> String {
|
|
success("git", root, args)
|
|
}
|
|
fn jj(root: &Path, args: &[&str]) -> String {
|
|
success("jj", root, args)
|
|
}
|
|
fn init_git(root: &Path) {
|
|
git(root, &["init", "--initial-branch=local"]);
|
|
git(root, &["config", "user.name", "Diff Test"]);
|
|
git(root, &["config", "user.email", "diff@example.invalid"]);
|
|
}
|
|
fn init_jj(root: &Path, colocate: bool) {
|
|
jj(
|
|
root,
|
|
&[
|
|
"git",
|
|
"init",
|
|
if colocate {
|
|
"--colocate"
|
|
} else {
|
|
"--no-colocate"
|
|
},
|
|
],
|
|
);
|
|
jj(root, &["config", "set", "--repo", "user.name", "Diff Test"]);
|
|
jj(
|
|
root,
|
|
&[
|
|
"config",
|
|
"set",
|
|
"--repo",
|
|
"user.email",
|
|
"diff@example.invalid",
|
|
],
|
|
);
|
|
}
|
|
fn open(root: &Path) -> Repository {
|
|
Repository::open(root, Arc::new(AtomicBool::new(false))).unwrap()
|
|
}
|
|
fn revision(id: &str, kind: RepoKind) -> Revision {
|
|
Revision {
|
|
target: RevisionTarget::Commit(id.into()),
|
|
label: id.into(),
|
|
description: String::new(),
|
|
category: if kind == RepoKind::Jujutsu {
|
|
Category::Revisions
|
|
} else {
|
|
Category::Commits
|
|
},
|
|
}
|
|
}
|
|
fn working(kind: RepoKind) -> Revision {
|
|
let mut r = revision("working", kind);
|
|
r.target = RevisionTarget::Working;
|
|
r
|
|
}
|
|
|
|
fn compare_loaded(
|
|
repository: &mut Repository,
|
|
before: &Revision,
|
|
after: &Revision,
|
|
buffers: &[(PathBuf, String)],
|
|
) -> Result<Comparison, String> {
|
|
let mut comparison =
|
|
repository.compare(1, before, after, ComparisonMode::Snapshots, buffers)?;
|
|
for (index, file) in comparison.files.iter_mut().enumerate() {
|
|
*file = repository.load_file(1, index)?;
|
|
if file
|
|
.notice
|
|
.as_deref()
|
|
.is_some_and(|notice| notice.contains("Binary or non-UTF-8"))
|
|
{
|
|
comparison.editable_paths.remove(&file.path);
|
|
}
|
|
}
|
|
Ok(comparison)
|
|
}
|
|
fn changed<'a>(comparison: &'a Comparison, path: &Path) -> &'a vamp::diff::ChangedFile {
|
|
comparison
|
|
.files
|
|
.iter()
|
|
.find(|f| f.path == path)
|
|
.unwrap_or_else(|| panic!("Missing changed file {path:?}: {:?}", comparison.files))
|
|
}
|
|
// Capture active metadata, checkout bytes, executable bits and link targets. Access
|
|
// times are intentionally excluded: reading a repository must not change its data.
|
|
fn disk_state(root: &Path) -> BTreeMap<PathBuf, (u32, Vec<u8>)> {
|
|
fn visit(root: &Path, path: &Path, result: &mut BTreeMap<PathBuf, (u32, Vec<u8>)>) {
|
|
for entry in fs::read_dir(path).unwrap() {
|
|
let entry = entry.unwrap();
|
|
let full = entry.path();
|
|
let metadata = fs::symlink_metadata(&full).unwrap();
|
|
if metadata.is_dir() {
|
|
visit(root, &full, result);
|
|
} else {
|
|
let data = if metadata.file_type().is_symlink() {
|
|
use std::os::unix::ffi::OsStrExt;
|
|
fs::read_link(&full)
|
|
.unwrap()
|
|
.as_os_str()
|
|
.as_bytes()
|
|
.to_vec()
|
|
} else {
|
|
fs::read(&full).unwrap()
|
|
};
|
|
result.insert(
|
|
full.strip_prefix(root).unwrap().to_path_buf(),
|
|
(metadata.permissions().mode(), data),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
let mut result = BTreeMap::new();
|
|
visit(root, root, &mut result);
|
|
result
|
|
}
|
|
|
|
#[test]
|
|
fn git_working_reads_disk_and_handles_ranges_ignored_files_binary_links_and_odd_paths() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("git");
|
|
init_git(&root);
|
|
for (path, data) in [
|
|
("text", &b"one\nthree\n"[..]),
|
|
("removed", b"removed\n"),
|
|
("empty-before", b""),
|
|
("newline", b"same\n"),
|
|
("crlf", b"a\r\n"),
|
|
("rename-old", b"move\n"),
|
|
("binary", b"\0old"),
|
|
("ignored-tracked", b"old\n"),
|
|
("executable", b"script\n"),
|
|
("type-switch", b"target"),
|
|
(".gitignore", b"ignored-*\n"),
|
|
] {
|
|
fs::write(root.join(path), data).unwrap();
|
|
}
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["add", "--force", "ignored-tracked"]);
|
|
git(&root, &["commit", "-m", "baseline"]);
|
|
git(&root, &["tag", "-a", "annotated", "-m", "tag"]);
|
|
git(
|
|
&root,
|
|
&["tag", "-a", "nested", "-m", "nested tag", "annotated"],
|
|
);
|
|
let id = git(&root, &["rev-parse", "HEAD"]);
|
|
fs::write(root.join("text"), b"one\nstaged\nthree\n").unwrap();
|
|
git(&root, &["add", "text"]);
|
|
fs::write(root.join("text"), b"one\ntwo\nthree\n").unwrap();
|
|
fs::remove_file(root.join("removed")).unwrap();
|
|
fs::write(root.join("empty-before"), b"first\n").unwrap();
|
|
fs::write(root.join("empty-added"), b"").unwrap();
|
|
fs::write(root.join("added"), b"added\n").unwrap();
|
|
fs::write(root.join("newline"), b"same").unwrap();
|
|
fs::write(root.join("crlf"), b"b\r\n").unwrap();
|
|
fs::write(root.join("binary"), b"\0new").unwrap();
|
|
fs::write(root.join("ignored-tracked"), b"changed\n").unwrap();
|
|
fs::write(root.join("ignored-untracked"), b"must not appear\n").unwrap();
|
|
fs::rename(root.join("rename-old"), root.join("rename-new")).unwrap();
|
|
fs::remove_file(root.join("type-switch")).unwrap();
|
|
symlink("target", root.join("type-switch")).unwrap();
|
|
fs::set_permissions(root.join("executable"), fs::Permissions::from_mode(0o755)).unwrap();
|
|
let odd = PathBuf::from("-tab\tand\nnewline");
|
|
fs::write(root.join(&odd), b"odd\n").unwrap();
|
|
let non_utf8 = PathBuf::from(OsString::from_vec(b"non-utf8-\xff".to_vec()));
|
|
let non_utf8_created = match fs::write(root.join(&non_utf8), b"bytes\n") {
|
|
Ok(()) => true,
|
|
// APFS rejects non-UTF-8 names before repository code can read them.
|
|
Err(error) if cfg!(target_os = "macos") && error.raw_os_error() == Some(92) => false,
|
|
Err(error) => panic!("Cannot create non-UTF-8 fixture: {error}"),
|
|
};
|
|
symlink("/not/a/path/to/read", root.join("link")).unwrap();
|
|
let state = disk_state(&root);
|
|
let mut repository = open(&root);
|
|
let catalog = repository.catalog().unwrap();
|
|
assert_eq!(catalog.kind, RepoKind::Git);
|
|
assert_eq!(catalog.entries[0].target, RevisionTarget::Working);
|
|
assert!(
|
|
catalog
|
|
.entries
|
|
.iter()
|
|
.any(|r| r.label == id && r.description == "baseline")
|
|
);
|
|
assert!(catalog.entries.iter().any(|r| r.label == "nested"
|
|
&& r.category == Category::Tags
|
|
&& r.target == RevisionTarget::Commit(id.clone())));
|
|
let comparison = compare_loaded(
|
|
&mut repository,
|
|
&revision(&id, RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
let text = changed(&comparison, Path::new("text"));
|
|
assert_eq!(comparison.root, fs::canonicalize(&root).unwrap());
|
|
assert!(comparison.editable_paths.contains(Path::new("text")));
|
|
assert!(comparison.editable_paths.contains(Path::new("empty-added")));
|
|
for path in ["binary", "link", "removed", "type-switch"] {
|
|
assert!(!comparison.editable_paths.contains(Path::new(path)));
|
|
}
|
|
assert_eq!(text.after, ["one", "two", "three"]);
|
|
assert_eq!(
|
|
(
|
|
text.hunks[0].before_start,
|
|
text.hunks[0].before_len,
|
|
text.hunks[0].after_start,
|
|
text.hunks[0].after_len
|
|
),
|
|
(1, 0, 1, 1)
|
|
);
|
|
let empty = changed(&comparison, Path::new("empty-before"));
|
|
assert_eq!(
|
|
(
|
|
empty.hunks[0].before_start,
|
|
empty.hunks[0].before_len,
|
|
empty.hunks[0].after_start,
|
|
empty.hunks[0].after_len
|
|
),
|
|
(0, 0, 0, 1)
|
|
);
|
|
let deleted = changed(&comparison, Path::new("removed"));
|
|
assert_eq!(deleted.status, FileStatus::Removed);
|
|
assert_eq!(
|
|
(
|
|
deleted.hunks[0].before_start,
|
|
deleted.hunks[0].before_len,
|
|
deleted.hunks[0].after_start,
|
|
deleted.hunks[0].after_len
|
|
),
|
|
(0, 1, 0, 0)
|
|
);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("empty-added")).status,
|
|
FileStatus::Added
|
|
);
|
|
assert!(
|
|
changed(&comparison, Path::new("empty-added"))
|
|
.after
|
|
.is_empty()
|
|
);
|
|
assert_eq!(changed(&comparison, &odd).after, ["odd"]);
|
|
if non_utf8_created {
|
|
assert_eq!(changed(&comparison, &non_utf8).after, ["bytes"]);
|
|
}
|
|
assert_eq!(changed(&comparison, Path::new("crlf")).after, ["b\r"]);
|
|
assert!(
|
|
changed(&comparison, Path::new("newline"))
|
|
.notice
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("EOF newline")
|
|
);
|
|
let type_switch = changed(&comparison, Path::new("type-switch"));
|
|
assert_eq!(type_switch.before, type_switch.after);
|
|
assert!(
|
|
type_switch
|
|
.notice
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("100644 → 120000")
|
|
);
|
|
let binary = changed(&comparison, Path::new("binary"));
|
|
assert!(binary.before.is_empty() && binary.after.is_empty() && binary.hunks.is_empty());
|
|
assert!(binary.notice.as_ref().unwrap().contains("Binary"));
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("link")).after,
|
|
["/not/a/path/to/read"]
|
|
);
|
|
assert!(
|
|
changed(&comparison, Path::new("link"))
|
|
.notice
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("Symbolic link")
|
|
);
|
|
assert!(
|
|
changed(&comparison, Path::new("executable"))
|
|
.notice
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("mode changed")
|
|
);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("ignored-tracked")).after,
|
|
["changed"]
|
|
);
|
|
assert!(
|
|
!comparison
|
|
.files
|
|
.iter()
|
|
.any(|f| f.path == Path::new("ignored-untracked"))
|
|
);
|
|
let moved = changed(&comparison, Path::new("rename-new"));
|
|
assert_eq!(moved.status, FileStatus::Renamed);
|
|
assert_eq!(moved.old_path.as_deref(), Some(Path::new("rename-old")));
|
|
assert_eq!(moved.before, moved.after);
|
|
assert!(moved.hunks.is_empty());
|
|
let reverse = compare_loaded(
|
|
&mut repository,
|
|
&working(RepoKind::Git),
|
|
&revision(&id, RepoKind::Git),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert!(reverse.editable_paths.is_empty());
|
|
assert_eq!(
|
|
changed(&reverse, Path::new("added")).status,
|
|
FileStatus::Removed
|
|
);
|
|
assert!(
|
|
repository
|
|
.compare(
|
|
1,
|
|
&working(RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[]
|
|
)
|
|
.unwrap()
|
|
.files
|
|
.is_empty()
|
|
);
|
|
assert_eq!(disk_state(&root), state);
|
|
}
|
|
|
|
#[test]
|
|
fn git_remote_refs_and_annotated_tags_are_discovered_before_fetch_and_history_stays_private() {
|
|
let fixture = Fixture::new();
|
|
let source = fixture.dir("source");
|
|
init_git(&source);
|
|
fs::write(source.join("file"), "ancestor\n").unwrap();
|
|
git(&source, &["add", "."]);
|
|
git(&source, &["commit", "-m", "remote ancestor needle"]);
|
|
let ancestor = git(&source, &["rev-parse", "HEAD"]);
|
|
git(&source, &["tag", "-a", "ancestor-tag", "-m", "annotated"]);
|
|
fs::write(source.join("file"), "remote tip\n").unwrap();
|
|
git(&source, &["commit", "-am", "remote tip"]);
|
|
let tip = git(&source, &["rev-parse", "HEAD"]);
|
|
git(&source, &["branch", "never-fetched"]);
|
|
let bare = fixture.0.join("remote.git");
|
|
git(
|
|
&fixture.0,
|
|
&[
|
|
"clone",
|
|
"--bare",
|
|
source.to_str().unwrap(),
|
|
bare.to_str().unwrap(),
|
|
],
|
|
);
|
|
let active = fixture.dir("active");
|
|
init_git(&active);
|
|
fs::write(active.join("file"), "local\n").unwrap();
|
|
git(&active, &["add", "."]);
|
|
git(&active, &["commit", "-m", "local"]);
|
|
let local = git(&active, &["rev-parse", "HEAD"]);
|
|
git(
|
|
&active,
|
|
&["remote", "add", "origin", bare.to_str().unwrap()],
|
|
);
|
|
let state = disk_state(&active);
|
|
let mut repository = open(&active);
|
|
let catalog = repository.list_remote("origin").unwrap();
|
|
let remote = catalog
|
|
.entries
|
|
.iter()
|
|
.find(|r| {
|
|
r.target
|
|
== (RevisionTarget::Remote {
|
|
remote: "origin".into(),
|
|
reference: "refs/heads/never-fetched".into(),
|
|
})
|
|
})
|
|
.unwrap()
|
|
.clone();
|
|
let tag = catalog
|
|
.entries
|
|
.iter()
|
|
.find(|r| {
|
|
r.target
|
|
== (RevisionTarget::Remote {
|
|
remote: "origin".into(),
|
|
reference: "refs/tags/ancestor-tag".into(),
|
|
})
|
|
})
|
|
.unwrap()
|
|
.clone();
|
|
assert!(
|
|
catalog
|
|
.entries
|
|
.iter()
|
|
.any(|r| r.target == RevisionTarget::FetchRemote("origin".into()))
|
|
);
|
|
assert!(
|
|
!catalog
|
|
.entries
|
|
.iter()
|
|
.any(|r| r.target == RevisionTarget::Commit(ancestor.clone()))
|
|
);
|
|
assert!(
|
|
!command("git", &active, &["cat-file", "-e", &tip])
|
|
.status
|
|
.success()
|
|
);
|
|
let comparison = compare_loaded(
|
|
&mut repository,
|
|
&revision(&local, RepoKind::Git),
|
|
&remote,
|
|
&[(active.join("file"), "unsaved local text\n".into())],
|
|
)
|
|
.unwrap();
|
|
assert!(comparison.editable_paths.is_empty());
|
|
assert_eq!(changed(&comparison, Path::new("file")).before, ["local"]);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("file")).after,
|
|
["remote tip"]
|
|
);
|
|
let comparison = compare_loaded(&mut repository, &tag, &remote, &[]).unwrap();
|
|
assert_eq!(changed(&comparison, Path::new("file")).before, ["ancestor"]);
|
|
let comparison =
|
|
compare_loaded(&mut repository, &remote, &working(RepoKind::Git), &[]).unwrap();
|
|
assert_eq!(changed(&comparison, Path::new("file")).after, ["local"]);
|
|
let loaded = repository.fetch_remote("origin").unwrap();
|
|
let historic = loaded
|
|
.entries
|
|
.iter()
|
|
.find(|r| {
|
|
r.target == RevisionTarget::Commit(ancestor.clone()) && r.category == Category::Commits
|
|
})
|
|
.unwrap();
|
|
assert_eq!(historic.label, ancestor);
|
|
assert_eq!(historic.description, "remote ancestor needle");
|
|
let comparison = compare_loaded(
|
|
&mut repository,
|
|
historic,
|
|
&revision(&local, RepoKind::Git),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert_eq!(changed(&comparison, Path::new("file")).before, ["ancestor"]);
|
|
assert!(
|
|
!command("git", &active, &["cat-file", "-e", &tip])
|
|
.status
|
|
.success()
|
|
);
|
|
assert_eq!(disk_state(&active), state);
|
|
}
|
|
|
|
#[test]
|
|
fn jj_catalog_and_working_comparisons_prefer_jj_and_never_snapshot_or_merge_operations() {
|
|
for colocate in [false, true] {
|
|
let fixture = Fixture::new();
|
|
let active = fixture.dir("active");
|
|
init_jj(&active, colocate);
|
|
fs::write(active.join("file"), "baseline\n").unwrap();
|
|
fs::write(active.join("deleted"), "delete\n").unwrap();
|
|
fs::write(active.join(".gitignore"), "ignored-*\n").unwrap();
|
|
jj(&active, &["describe", "-m", "jj baseline searchable"]);
|
|
let id = jj(
|
|
&active,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"--at-operation=@",
|
|
"log",
|
|
"--no-graph",
|
|
"-r",
|
|
"@",
|
|
"-T",
|
|
"commit_id",
|
|
],
|
|
);
|
|
jj(&active, &["bookmark", "create", "baseline"]);
|
|
jj(&active, &["new"]);
|
|
fs::write(active.join("file"), "disk\n").unwrap();
|
|
fs::remove_file(active.join("deleted")).unwrap();
|
|
fs::write(active.join("addition"), "new\n").unwrap();
|
|
fs::write(active.join("ignored-untracked"), "ignored\n").unwrap();
|
|
let state = disk_state(&active);
|
|
let mut repository = open(&active);
|
|
let catalog = repository.catalog().unwrap();
|
|
assert_eq!(catalog.kind, RepoKind::Jujutsu);
|
|
let baseline = catalog
|
|
.entries
|
|
.iter()
|
|
.find(|r| {
|
|
r.target == RevisionTarget::Commit(id.clone()) && r.category == Category::Revisions
|
|
})
|
|
.unwrap();
|
|
assert!(
|
|
catalog
|
|
.entries
|
|
.iter()
|
|
.any(|r| r.label == "baseline" && r.category == Category::Bookmarks)
|
|
);
|
|
let comparison =
|
|
compare_loaded(&mut repository, baseline, &working(RepoKind::Jujutsu), &[]).unwrap();
|
|
assert_eq!(changed(&comparison, Path::new("file")).after, ["disk"]);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("deleted")).status,
|
|
FileStatus::Removed
|
|
);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("addition")).status,
|
|
FileStatus::Added
|
|
);
|
|
assert!(
|
|
!comparison
|
|
.files
|
|
.iter()
|
|
.any(|f| f.path == Path::new("ignored-untracked"))
|
|
);
|
|
let synthetic_root = revision(&"0".repeat(40), RepoKind::Jujutsu);
|
|
let branch = repository
|
|
.compare(
|
|
20,
|
|
&synthetic_root,
|
|
baseline,
|
|
ComparisonMode::BranchChanges,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert!(branch.before.ends_with(&"0".repeat(40)));
|
|
assert!(
|
|
branch
|
|
.files
|
|
.iter()
|
|
.any(|file| file.path == Path::new("file"))
|
|
);
|
|
let reversed = repository
|
|
.compare(
|
|
21,
|
|
baseline,
|
|
&synthetic_root,
|
|
ComparisonMode::BranchChanges,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert!(reversed.files.is_empty());
|
|
assert!(
|
|
repository
|
|
.compare(22, baseline, baseline, ComparisonMode::BranchChanges, &[])
|
|
.unwrap()
|
|
.files
|
|
.is_empty()
|
|
);
|
|
assert_eq!(disk_state(&active), state);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn jj_private_fetch_discovers_never_fetched_bookmark_and_searchable_remote_change_history() {
|
|
let fixture = Fixture::new();
|
|
let source = fixture.dir("source");
|
|
init_jj(&source, false);
|
|
fs::write(source.join("file"), "remote ancestor\n").unwrap();
|
|
jj(&source, &["describe", "-m", "remote historic needle"]);
|
|
let ancestor = jj(
|
|
&source,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"log",
|
|
"--no-graph",
|
|
"-r",
|
|
"@",
|
|
"-T",
|
|
"commit_id",
|
|
],
|
|
);
|
|
jj(&source, &["new"]);
|
|
fs::write(source.join("file"), "remote tip\n").unwrap();
|
|
jj(&source, &["describe", "-m", "remote tip"]);
|
|
let tip = jj(
|
|
&source,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"log",
|
|
"--no-graph",
|
|
"-r",
|
|
"@",
|
|
"-T",
|
|
"commit_id",
|
|
],
|
|
);
|
|
jj(&source, &["bookmark", "create", "never-fetched"]);
|
|
let bare = fixture.dir("remote.git");
|
|
jj(&source, &["git", "export"]);
|
|
// Build remote fixture data by copying exported refs, never by pushing.
|
|
git(
|
|
&fixture.0,
|
|
&[
|
|
"clone",
|
|
"--bare",
|
|
"--no-hardlinks",
|
|
source.join(".jj/repo/store/git").to_str().unwrap(),
|
|
bare.to_str().unwrap(),
|
|
],
|
|
);
|
|
let active = fixture.dir("active");
|
|
init_jj(&active, false);
|
|
fs::write(active.join("file"), "local\n").unwrap();
|
|
jj(&active, &["describe", "-m", "local"]);
|
|
jj(&active, &["new"]);
|
|
jj(
|
|
&active,
|
|
&["git", "remote", "add", "origin", bare.to_str().unwrap()],
|
|
);
|
|
fs::write(active.join("unsnapshotted"), "keep disk\n").unwrap();
|
|
let state = disk_state(&active);
|
|
let mut repository = open(&active);
|
|
let catalog = repository.list_remote("origin").unwrap();
|
|
let remote = catalog
|
|
.entries
|
|
.iter()
|
|
.find(|r| {
|
|
r.target
|
|
== (RevisionTarget::Remote {
|
|
remote: "origin".into(),
|
|
reference: "refs/heads/never-fetched".into(),
|
|
})
|
|
})
|
|
.unwrap()
|
|
.clone();
|
|
assert!(
|
|
!catalog
|
|
.entries
|
|
.iter()
|
|
.any(|r| r.target == RevisionTarget::Commit(ancestor.clone()))
|
|
);
|
|
let local = catalog.entries.iter().find(|revision| {
|
|
matches!(&revision.target, RevisionTarget::Commit(id) if id.bytes().any(|byte| byte != b'0'))
|
|
}).unwrap();
|
|
let branch = repository
|
|
.compare(60, local, &remote, ComparisonMode::BranchChanges, &[])
|
|
.unwrap();
|
|
assert!(branch.before.ends_with(&"0".repeat(40)));
|
|
assert_eq!(branch.files.len(), 1);
|
|
assert_eq!(repository.load_file(60, 0).unwrap().after, ["remote tip"]);
|
|
let comparison =
|
|
compare_loaded(&mut repository, &remote, &working(RepoKind::Jujutsu), &[]).unwrap();
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("file")).before,
|
|
["remote tip"]
|
|
);
|
|
assert_eq!(changed(&comparison, Path::new("file")).after, ["local"]);
|
|
let loaded = repository.fetch_remote("origin").unwrap();
|
|
let historic = loaded
|
|
.entries
|
|
.iter()
|
|
.find(|r| {
|
|
r.target == RevisionTarget::Commit(ancestor.clone())
|
|
&& r.category == Category::Revisions
|
|
})
|
|
.unwrap();
|
|
let comparison = compare_loaded(&mut repository, historic, &remote, &[]).unwrap();
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("file")).before,
|
|
["remote ancestor"]
|
|
);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("file")).after,
|
|
["remote tip"]
|
|
);
|
|
let branch = repository
|
|
.compare(61, historic, &remote, ComparisonMode::BranchChanges, &[])
|
|
.unwrap();
|
|
assert!(branch.before.ends_with(&ancestor));
|
|
assert_eq!(
|
|
repository.load_file(61, 0).unwrap().before,
|
|
["remote ancestor"]
|
|
);
|
|
let store = active.join(".jj/repo/store");
|
|
let active_store =
|
|
fs::canonicalize(store.join(fs::read_to_string(store.join("git_target")).unwrap().trim()))
|
|
.unwrap();
|
|
assert!(
|
|
!command(
|
|
"git",
|
|
&active,
|
|
&[
|
|
"--git-dir",
|
|
active_store.to_str().unwrap(),
|
|
"cat-file",
|
|
"-e",
|
|
&tip
|
|
]
|
|
)
|
|
.status
|
|
.success()
|
|
);
|
|
assert_eq!(disk_state(&active), state);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_are_explicit_and_do_not_modify_active_repository() {
|
|
let fixture = Fixture::new();
|
|
assert!(Repository::open(&fixture.0, Arc::new(AtomicBool::new(false))).is_err());
|
|
let root = fixture.dir("active");
|
|
init_git(&root);
|
|
fs::write(root.join("file"), "baseline\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "baseline"]);
|
|
let id = git(&root, &["rev-parse", "HEAD"]);
|
|
git(
|
|
&root,
|
|
&[
|
|
"remote",
|
|
"add",
|
|
"broken",
|
|
fixture.0.join("missing.git").to_str().unwrap(),
|
|
],
|
|
);
|
|
let state = disk_state(&root);
|
|
let mut repository = open(&root);
|
|
let catalog = repository.catalog().unwrap();
|
|
assert!(catalog.notice.is_none());
|
|
assert!(
|
|
catalog
|
|
.entries
|
|
.iter()
|
|
.any(|revision| revision.target == RevisionTarget::ListRemote("broken".into()))
|
|
);
|
|
assert!(repository.list_remote("broken").is_err());
|
|
assert!(repository.fetch_remote("broken").is_err());
|
|
assert!(
|
|
repository
|
|
.fetch_remote("unknown")
|
|
.unwrap_err()
|
|
.contains("Unknown configured remote")
|
|
);
|
|
assert!(
|
|
repository
|
|
.compare(
|
|
1,
|
|
&revision("--bad", RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[]
|
|
)
|
|
.unwrap_err()
|
|
.contains("Invalid immutable commit")
|
|
);
|
|
assert!(
|
|
repository
|
|
.compare(
|
|
1,
|
|
&revision(&"f".repeat(40), RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[],
|
|
)
|
|
.unwrap_err()
|
|
.contains("unavailable")
|
|
);
|
|
let loader = Revision {
|
|
target: RevisionTarget::FetchRemote("broken".into()),
|
|
..working(RepoKind::Git)
|
|
};
|
|
assert!(
|
|
repository
|
|
.compare(
|
|
1,
|
|
&loader,
|
|
&revision(&id, RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[]
|
|
)
|
|
.unwrap_err()
|
|
.contains("history loader")
|
|
);
|
|
assert!(
|
|
Repository::open(&root, Arc::new(AtomicBool::new(true)))
|
|
.err()
|
|
.unwrap()
|
|
.contains("cancelled")
|
|
);
|
|
assert_eq!(disk_state(&root), state);
|
|
}
|
|
|
|
#[test]
|
|
fn unchanged_submodules_are_not_false_changes_and_changed_pointers_are_explicit() {
|
|
let fixture = Fixture::new();
|
|
let child = fixture.dir("child");
|
|
init_git(&child);
|
|
fs::write(child.join("file"), "first\n").unwrap();
|
|
git(&child, &["add", "."]);
|
|
git(&child, &["commit", "-m", "first"]);
|
|
let first = git(&child, &["rev-parse", "HEAD"]);
|
|
let root = fixture.dir("active");
|
|
init_git(&root);
|
|
git(
|
|
&root,
|
|
&[
|
|
"-c",
|
|
"protocol.file.allow=always",
|
|
"submodule",
|
|
"add",
|
|
child.to_str().unwrap(),
|
|
"nested",
|
|
],
|
|
);
|
|
git(&root, &["commit", "-m", "submodule"]);
|
|
let baseline = git(&root, &["rev-parse", "HEAD"]);
|
|
let mut repository = open(&root);
|
|
assert!(
|
|
repository
|
|
.compare(
|
|
1,
|
|
&revision(&baseline, RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[]
|
|
)
|
|
.unwrap()
|
|
.files
|
|
.is_empty()
|
|
);
|
|
fs::write(child.join("file"), "second\n").unwrap();
|
|
git(&child, &["commit", "-am", "second"]);
|
|
let second = git(&child, &["rev-parse", "HEAD"]);
|
|
git(&root.join("nested"), &["fetch", "origin"]);
|
|
git(&root.join("nested"), &["checkout", "--detach", &second]);
|
|
let state = disk_state(&root);
|
|
let comparison = compare_loaded(
|
|
&mut repository,
|
|
&revision(&baseline, RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
let submodule = changed(&comparison, Path::new("nested"));
|
|
assert_eq!(submodule.status, FileStatus::Modified);
|
|
let notice = submodule.notice.as_ref().unwrap();
|
|
assert!(
|
|
notice.contains(&first) && notice.contains(&second) && notice.contains("not traversed")
|
|
);
|
|
assert!(submodule.before.is_empty() && submodule.after.is_empty());
|
|
assert!(
|
|
!comparison
|
|
.files
|
|
.iter()
|
|
.any(|f| f.path.starts_with("nested/file"))
|
|
);
|
|
assert_eq!(disk_state(&root), state);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_remotes_retain_tag_only_history_even_when_tag_names_collide() {
|
|
let fixture = Fixture::new();
|
|
let mut ids = vec![];
|
|
let mut urls = vec![];
|
|
for name in ["one", "two"] {
|
|
let source = fixture.dir(name);
|
|
init_git(&source);
|
|
fs::write(source.join("file"), format!("{name}\n")).unwrap();
|
|
git(&source, &["add", "."]);
|
|
git(
|
|
&source,
|
|
&["commit", "-m", &format!("{name} tag-only needle")],
|
|
);
|
|
ids.push(git(&source, &["rev-parse", "HEAD"]));
|
|
git(&source, &["tag", "collision"]);
|
|
git(&source, &["update-ref", "-d", "refs/heads/local"]);
|
|
let bare = fixture.0.join(format!("{name}.git"));
|
|
git(
|
|
&fixture.0,
|
|
&[
|
|
"clone",
|
|
"--bare",
|
|
source.to_str().unwrap(),
|
|
bare.to_str().unwrap(),
|
|
],
|
|
);
|
|
urls.push(bare);
|
|
}
|
|
let active = fixture.dir("active");
|
|
init_git(&active);
|
|
fs::write(active.join("local"), "local\n").unwrap();
|
|
git(&active, &["add", "."]);
|
|
git(&active, &["commit", "-m", "local"]);
|
|
for (name, url) in ["one", "two"].into_iter().zip(&urls) {
|
|
git(&active, &["remote", "add", name, url.to_str().unwrap()]);
|
|
}
|
|
let state = disk_state(&active);
|
|
let mut repository = open(&active);
|
|
repository.fetch_remote("one").unwrap();
|
|
let loaded = repository.fetch_remote("two").unwrap();
|
|
for (name, id) in ["one", "two"].into_iter().zip(&ids) {
|
|
assert!(
|
|
loaded
|
|
.entries
|
|
.iter()
|
|
.any(|r| r.target == RevisionTarget::Commit(id.clone())
|
|
&& r.description == format!("{name} tag-only needle"))
|
|
);
|
|
}
|
|
let comparison = compare_loaded(
|
|
&mut repository,
|
|
&revision(&ids[0], RepoKind::Git),
|
|
&revision(&ids[1], RepoKind::Git),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert_eq!(changed(&comparison, Path::new("file")).before, ["one"]);
|
|
assert_eq!(changed(&comparison, Path::new("file")).after, ["two"]);
|
|
assert_eq!(disk_state(&active), state);
|
|
}
|
|
|
|
#[test]
|
|
fn jj_conflicted_revisions_show_cli_projection_and_keep_ordinary_files_navigable() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("active");
|
|
init_jj(&root, false);
|
|
fs::write(root.join("conflict"), "base\n").unwrap();
|
|
fs::write(root.join("ordinary"), "old\n").unwrap();
|
|
jj(&root, &["describe", "-m", "base"]);
|
|
let base = jj(
|
|
&root,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"log",
|
|
"--no-graph",
|
|
"-r",
|
|
"@",
|
|
"-T",
|
|
"commit_id",
|
|
],
|
|
);
|
|
jj(&root, &["new", &base]);
|
|
fs::write(root.join("conflict"), "left\n").unwrap();
|
|
fs::write(root.join("ordinary"), "left ordinary\n").unwrap();
|
|
jj(&root, &["describe", "-m", "left"]);
|
|
let left = jj(
|
|
&root,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"log",
|
|
"--no-graph",
|
|
"-r",
|
|
"@",
|
|
"-T",
|
|
"commit_id",
|
|
],
|
|
);
|
|
jj(&root, &["new", &base]);
|
|
fs::write(root.join("conflict"), "right\n").unwrap();
|
|
jj(&root, &["describe", "-m", "right"]);
|
|
let right = jj(
|
|
&root,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"log",
|
|
"--no-graph",
|
|
"-r",
|
|
"@",
|
|
"-T",
|
|
"commit_id",
|
|
],
|
|
);
|
|
jj(&root, &["new", &left, &right, "-m", "conflicted merge"]);
|
|
let merged = jj(
|
|
&root,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"--at-operation=@",
|
|
"log",
|
|
"--no-graph",
|
|
"-r",
|
|
"@",
|
|
"-T",
|
|
"commit_id",
|
|
],
|
|
);
|
|
let projection = command(
|
|
"jj",
|
|
&root,
|
|
&[
|
|
"--ignore-working-copy",
|
|
"--at-operation=@",
|
|
"file",
|
|
"show",
|
|
"-r",
|
|
&merged,
|
|
"-T",
|
|
"",
|
|
"--",
|
|
"root-file:\"conflict\"",
|
|
],
|
|
);
|
|
assert!(
|
|
projection.status.success(),
|
|
"{}",
|
|
String::from_utf8_lossy(&projection.stderr)
|
|
);
|
|
let projection = String::from_utf8(projection.stdout).unwrap();
|
|
let expected: Vec<_> = projection
|
|
.strip_suffix('\n')
|
|
.unwrap_or(&projection)
|
|
.split('\n')
|
|
.map(str::to_owned)
|
|
.collect();
|
|
let state = disk_state(&root);
|
|
let mut repository = open(&root);
|
|
let catalog = repository.catalog().unwrap();
|
|
assert!(
|
|
catalog
|
|
.entries
|
|
.iter()
|
|
.any(|r| r.target == RevisionTarget::Commit(merged.clone()))
|
|
);
|
|
let comparison = compare_loaded(
|
|
&mut repository,
|
|
&revision(&base, RepoKind::Jujutsu),
|
|
&revision(&merged, RepoKind::Jujutsu),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
let conflict = changed(&comparison, Path::new("conflict"));
|
|
assert_eq!(conflict.after, expected);
|
|
assert!(
|
|
conflict
|
|
.notice
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("conflict projection")
|
|
);
|
|
assert!(!conflict.hunks.is_empty());
|
|
let ordinary = changed(&comparison, Path::new("ordinary"));
|
|
assert_eq!(ordinary.before, ["old"]);
|
|
assert_eq!(ordinary.after, ["left ordinary"]);
|
|
assert!(ordinary.notice.is_none());
|
|
let branch = repository
|
|
.compare(
|
|
30,
|
|
&revision(&base, RepoKind::Jujutsu),
|
|
&revision(&merged, RepoKind::Jujutsu),
|
|
ComparisonMode::BranchChanges,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
let index = branch
|
|
.files
|
|
.iter()
|
|
.position(|file| file.path == Path::new("conflict"))
|
|
.unwrap();
|
|
let conflict = repository.load_file(30, index).unwrap();
|
|
assert_eq!(conflict.after, expected);
|
|
assert!(
|
|
conflict
|
|
.notice
|
|
.as_deref()
|
|
.unwrap()
|
|
.contains("conflict projection")
|
|
);
|
|
let working_comparison = compare_loaded(
|
|
&mut repository,
|
|
&revision(&merged, RepoKind::Jujutsu),
|
|
&working(RepoKind::Jujutsu),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
changed(&working_comparison, Path::new("conflict"))
|
|
.notice
|
|
.as_ref()
|
|
.unwrap()
|
|
.contains("conflict projection")
|
|
);
|
|
assert_eq!(disk_state(&root), state);
|
|
}
|
|
|
|
#[test]
|
|
fn unsaved_overlays_are_working_after_only_and_cannot_follow_nonregular_paths() {
|
|
let fixture = Fixture::new();
|
|
let root = fs::canonicalize(fixture.dir("overlay")).unwrap();
|
|
init_git(&root);
|
|
fs::create_dir(root.join("sub")).unwrap();
|
|
fs::write(root.join("file"), "base\n").unwrap();
|
|
fs::write(root.join("sub/file"), "nested\n").unwrap();
|
|
fs::write(root.join("binary"), b"\0binary").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "base"]);
|
|
let id = git(&root, &["rev-parse", "HEAD"]);
|
|
let outside = fixture.dir("outside");
|
|
fs::write(outside.join("file"), "outside\n").unwrap();
|
|
symlink(&outside, root.join("linked")).unwrap();
|
|
symlink("file", root.join("link")).unwrap();
|
|
let state = disk_state(&root);
|
|
let buffers = vec![
|
|
(root.join("file"), "unsaved\n".into()),
|
|
(root.join("binary"), "not binary\n".into()),
|
|
(root.join("missing"), "not on disk\n".into()),
|
|
(root.join("link"), "not a regular file\n".into()),
|
|
(root.join("linked/file"), "outside via link\n".into()),
|
|
(outside.join("file"), "outside\n".into()),
|
|
(root.join("sub/../../outside/file"), "escaped\n".into()),
|
|
(root.join(".git/config"), "metadata\n".into()),
|
|
];
|
|
let mut repository = open(&root.join("sub"));
|
|
let base = revision(&id, RepoKind::Git);
|
|
let checkout = working(RepoKind::Git);
|
|
let comparison = compare_loaded(&mut repository, &base, &checkout, &buffers).unwrap();
|
|
assert_eq!(comparison.root, fs::canonicalize(&root).unwrap());
|
|
assert_eq!(changed(&comparison, Path::new("file")).after, ["unsaved"]);
|
|
assert!(comparison.editable_paths.contains(Path::new("file")));
|
|
for path in [
|
|
"binary",
|
|
"missing",
|
|
"link",
|
|
"linked",
|
|
"linked/file",
|
|
".git/config",
|
|
] {
|
|
assert!(!comparison.editable_paths.contains(Path::new(path)));
|
|
}
|
|
assert!(
|
|
!comparison
|
|
.files
|
|
.iter()
|
|
.any(|file| file.path == Path::new("binary")
|
|
|| file.path == Path::new("missing")
|
|
|| file.path == Path::new("linked/file"))
|
|
);
|
|
let immutable = compare_loaded(&mut repository, &checkout, &base, &buffers).unwrap();
|
|
assert!(immutable.editable_paths.is_empty());
|
|
assert!(
|
|
!immutable
|
|
.files
|
|
.iter()
|
|
.any(|file| file.path == Path::new("file"))
|
|
);
|
|
assert!(
|
|
repository
|
|
.compare(1, &base, &base, ComparisonMode::Snapshots, &buffers)
|
|
.unwrap()
|
|
.files
|
|
.is_empty()
|
|
);
|
|
let same_checkout = compare_loaded(&mut repository, &checkout, &checkout, &buffers).unwrap();
|
|
assert_eq!(changed(&same_checkout, Path::new("file")).before, ["base"]);
|
|
assert_eq!(
|
|
changed(&same_checkout, Path::new("file")).after,
|
|
["unsaved"]
|
|
);
|
|
assert_eq!(disk_state(&root), state);
|
|
assert_eq!(
|
|
fs::read_to_string(outside.join("file")).unwrap(),
|
|
"outside\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn immutable_catalog_skips_unchanged_blobs_and_prepares_only_requested_details() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("lazy");
|
|
init_git(&root);
|
|
fs::write(root.join("unchanged-large"), vec![b'x'; 17 * 1024 * 1024]).unwrap();
|
|
fs::write(root.join("selected"), "before\n").unwrap();
|
|
fs::write(root.join("unselected"), "old\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "before"]);
|
|
let before = git(&root, &["rev-parse", "HEAD"]);
|
|
fs::write(root.join("selected"), "after\n").unwrap();
|
|
fs::write(root.join("unselected"), vec![b'y'; 17 * 1024 * 1024]).unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "after"]);
|
|
let after = git(&root, &["rev-parse", "HEAD"]);
|
|
let mut repository = open(&root);
|
|
let comparison = repository
|
|
.compare(
|
|
100,
|
|
&revision(&before, RepoKind::Git),
|
|
&revision(&after, RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert_eq!(comparison.files.len(), 2);
|
|
assert!(comparison.files.iter().all(|file| !file.loaded
|
|
&& file.before.is_empty()
|
|
&& file.after.is_empty()
|
|
&& file.hunks.is_empty()));
|
|
let index = comparison
|
|
.files
|
|
.iter()
|
|
.position(|file| file.path == Path::new("selected"))
|
|
.unwrap();
|
|
let selected = repository.load_file(100, index).unwrap();
|
|
assert!(selected.loaded);
|
|
assert_eq!(selected.before, ["before"]);
|
|
assert_eq!(selected.after, ["after"]);
|
|
assert_eq!(selected.hunks.len(), 1);
|
|
assert!(repository.load_file(99, index).is_err());
|
|
let empty = repository
|
|
.compare(
|
|
101,
|
|
&revision(&after, RepoKind::Git),
|
|
&revision(&after, RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert!(empty.files.is_empty());
|
|
assert!(repository.load_file(100, index).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn deferred_file_details_keep_frozen_checkout_and_unsaved_after() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("frozen").canonicalize().unwrap();
|
|
init_git(&root);
|
|
fs::write(root.join("file"), "committed\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "before"]);
|
|
fs::write(root.join("file"), "disk before\n").unwrap();
|
|
let mut repository = open(&root);
|
|
let comparison = repository
|
|
.compare(
|
|
50,
|
|
&working(RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[(root.join("file"), "unsaved after\n".into())],
|
|
)
|
|
.unwrap();
|
|
assert_eq!(comparison.files.len(), 1);
|
|
assert!(!comparison.files[0].loaded);
|
|
fs::write(root.join("file"), "later disk\n").unwrap();
|
|
let details = repository.load_file(50, 0).unwrap();
|
|
assert_eq!(details.before, ["disk before"]);
|
|
assert_eq!(details.after, ["unsaved after"]);
|
|
let again = repository.load_file(50, 0).unwrap();
|
|
assert_eq!(again.before, details.before);
|
|
assert_eq!(again.after, details.after);
|
|
}
|
|
|
|
#[test]
|
|
fn git_working_after_advances_only_to_descendants_with_new_paths_and_unsaved_text() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("follow").canonicalize().unwrap();
|
|
init_git(&root);
|
|
fs::write(root.join("file"), "base\n").unwrap();
|
|
fs::write(root.join("removed"), "remove me\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "base"]);
|
|
let base = git(&root, &["rev-parse", "HEAD"]);
|
|
fs::write(root.join("file"), "original disk\n").unwrap();
|
|
let mut repository = open(&root);
|
|
let original = compare_loaded(
|
|
&mut repository,
|
|
&revision(&base, RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
fs::write(root.join("file"), "later disk\n").unwrap();
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(1, 2, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
assert_eq!(repository.load_file(1, 0).unwrap().after, ["original disk"]);
|
|
fs::remove_file(root.join("removed")).unwrap();
|
|
fs::write(root.join("added"), "new file\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "descendant"]);
|
|
let tip = git(&root, &["rev-parse", "HEAD"]);
|
|
let state = disk_state(&root);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(99, 2, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
let mut refreshed = repository
|
|
.refresh_working_after(1, 2, &[(root.join("file"), "unsaved\n".into())])
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(refreshed.before, original.before);
|
|
assert_eq!(refreshed.after, original.after);
|
|
for (index, file) in refreshed.files.iter_mut().enumerate() {
|
|
*file = repository.load_file(2, index).unwrap();
|
|
}
|
|
assert_eq!(changed(&refreshed, Path::new("file")).before, ["base"]);
|
|
assert_eq!(changed(&refreshed, Path::new("file")).after, ["unsaved"]);
|
|
assert_eq!(
|
|
changed(&refreshed, Path::new("added")).status,
|
|
FileStatus::Added
|
|
);
|
|
assert_eq!(
|
|
changed(&refreshed, Path::new("removed")).status,
|
|
FileStatus::Removed
|
|
);
|
|
assert!(refreshed.editable_paths.contains(Path::new("file")));
|
|
assert!(refreshed.editable_paths.contains(Path::new("added")));
|
|
assert!(!refreshed.editable_paths.contains(Path::new("removed")));
|
|
assert!(repository.load_file(1, 0).is_err());
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(2, 3, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
assert_eq!(disk_state(&root), state);
|
|
git(&root, &["checkout", "--detach", &base]);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(2, 3, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
git(&root, &["checkout", "-b", "other"]);
|
|
fs::write(root.join("file"), "other branch\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "unrelated to tip"]);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(2, 3, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
let index = refreshed
|
|
.files
|
|
.iter()
|
|
.position(|file| file.path == Path::new("file"))
|
|
.unwrap();
|
|
assert_eq!(repository.load_file(2, index).unwrap().after, ["unsaved"]);
|
|
git(&root, &["checkout", "--detach", &tip]);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(2, 3, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn working_after_refresh_preserves_frozen_working_before_and_sources_on_failure() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("frozen-follow").canonicalize().unwrap();
|
|
init_git(&root);
|
|
fs::write(root.join("file"), "base\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "base"]);
|
|
let base = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
|
|
fs::write(root.join("file"), "frozen before\n").unwrap();
|
|
let cancelled = Arc::new(AtomicBool::new(false));
|
|
let mut repository = Repository::open(&root, cancelled.clone()).unwrap();
|
|
repository
|
|
.compare(
|
|
10,
|
|
&working(RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[(root.join("file"), "frozen after\n".into())],
|
|
)
|
|
.unwrap();
|
|
fs::write(root.join("file"), "next checkout\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "next"]);
|
|
cancelled.store(true, Ordering::Release);
|
|
assert!(repository.refresh_working_after(10, 11, &[]).is_err());
|
|
cancelled.store(false, Ordering::Release);
|
|
assert_eq!(repository.load_file(10, 0).unwrap().after, ["frozen after"]);
|
|
fs::write(root.join("oversized"), vec![b'x'; 17 * 1024 * 1024]).unwrap();
|
|
assert!(repository.refresh_working_after(10, 11, &[]).is_err());
|
|
let original = repository.load_file(10, 0).unwrap();
|
|
assert_eq!(original.before, ["frozen before"]);
|
|
assert_eq!(original.after, ["frozen after"]);
|
|
assert!(repository.load_file(11, 0).is_err());
|
|
fs::remove_file(root.join("oversized")).unwrap();
|
|
let refreshed = repository
|
|
.refresh_working_after(10, 11, &[])
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(refreshed.files.len(), 1);
|
|
let file = repository.load_file(11, 0).unwrap();
|
|
assert_eq!(file.before, ["frozen before"]);
|
|
assert_eq!(file.after, ["next checkout"]);
|
|
let tip = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
|
|
repository
|
|
.compare(12, &base, &tip, ComparisonMode::Snapshots, &[])
|
|
.unwrap();
|
|
fs::write(root.join("file"), "newer checkout\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "newer"]);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(12, 13, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
assert_eq!(
|
|
repository.load_file(12, 0).unwrap().after,
|
|
["next checkout"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn refresh_materialization_failure_restores_prepared_file_sources() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("failed-materialization");
|
|
init_git(&root);
|
|
fs::write(root.join("a-file"), "frozen source\n").unwrap();
|
|
fs::write(root.join("z-large"), vec![b'x'; 17 * 1024 * 1024]).unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "base"]);
|
|
let base = revision(&git(&root, &["rev-parse", "HEAD"]), RepoKind::Git);
|
|
fs::remove_file(root.join("a-file")).unwrap();
|
|
fs::remove_file(root.join("z-large")).unwrap();
|
|
let mut repository = open(&root);
|
|
let original = repository
|
|
.compare(
|
|
1,
|
|
&base,
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
let index = original
|
|
.files
|
|
.iter()
|
|
.position(|file| file.path == Path::new("a-file"))
|
|
.unwrap();
|
|
fs::write(root.join("a-file"), "new checkout\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "descendant"]);
|
|
assert!(repository.refresh_working_after(1, 2, &[]).is_err());
|
|
let file = repository.load_file(1, index).unwrap();
|
|
assert_eq!(file.before, ["frozen source"]);
|
|
assert!(file.after.is_empty());
|
|
assert_eq!(file.status, FileStatus::Removed);
|
|
assert!(repository.load_file(2, index).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn unborn_git_working_after_safely_advances_to_its_first_commit() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("unborn");
|
|
init_git(&root);
|
|
let mut repository = open(&root);
|
|
repository
|
|
.compare(
|
|
1,
|
|
&working(RepoKind::Git),
|
|
&working(RepoKind::Git),
|
|
ComparisonMode::Snapshots,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(1, 2, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
fs::write(root.join("first"), "initial\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "initial"]);
|
|
let comparison = repository
|
|
.refresh_working_after(1, 2, &[])
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(comparison.files.len(), 1);
|
|
assert_eq!(repository.load_file(2, 0).unwrap().after, ["initial"]);
|
|
}
|
|
|
|
#[test]
|
|
fn jj_working_after_follows_changes_not_rewrites_and_rejects_other_history() {
|
|
for colocate in [false, true] {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("jj-follow").canonicalize().unwrap();
|
|
init_jj(&root, colocate);
|
|
fs::write(root.join("file"), "base\n").unwrap();
|
|
fs::write(root.join("removed"), "remove me\n").unwrap();
|
|
jj(&root, &["describe", "-m", "base"]);
|
|
let base = jj(&root, &["log", "--no-graph", "-r", "@", "-T", "commit_id"]);
|
|
jj(&root, &["new"]);
|
|
fs::write(root.join("file"), "original disk\n").unwrap();
|
|
let mut repository = open(&root);
|
|
compare_loaded(
|
|
&mut repository,
|
|
&revision(&base, RepoKind::Jujutsu),
|
|
&working(RepoKind::Jujutsu),
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
fs::write(root.join("file"), "same-change rewrite\n").unwrap();
|
|
jj(&root, &["describe", "-m", "rewritten"]);
|
|
let state = disk_state(&root);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(1, 2, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
assert_eq!(repository.load_file(1, 0).unwrap().after, ["original disk"]);
|
|
assert_eq!(disk_state(&root), state);
|
|
jj(&root, &["new"]);
|
|
fs::remove_file(root.join("removed")).unwrap();
|
|
fs::write(root.join("added"), "new file\n").unwrap();
|
|
let state = disk_state(&root);
|
|
let mut comparison = repository
|
|
.refresh_working_after(1, 2, &[(root.join("file"), "unsaved\n".into())])
|
|
.unwrap()
|
|
.unwrap();
|
|
for (index, file) in comparison.files.iter_mut().enumerate() {
|
|
*file = repository.load_file(2, index).unwrap();
|
|
}
|
|
assert_eq!(changed(&comparison, Path::new("file")).before, ["base"]);
|
|
assert_eq!(changed(&comparison, Path::new("file")).after, ["unsaved"]);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("added")).status,
|
|
FileStatus::Added
|
|
);
|
|
assert_eq!(
|
|
changed(&comparison, Path::new("removed")).status,
|
|
FileStatus::Removed
|
|
);
|
|
assert!(repository.load_file(1, 0).is_err());
|
|
assert_eq!(disk_state(&root), state);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(2, 3, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
jj(&root, &["edit", &base]);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(2, 3, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
jj(&root, &["new", &base]);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(2, 3, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
let index = comparison
|
|
.files
|
|
.iter()
|
|
.position(|file| file.path == Path::new("file"))
|
|
.unwrap();
|
|
assert_eq!(repository.load_file(2, index).unwrap().after, ["unsaved"]);
|
|
repository
|
|
.compare(
|
|
10,
|
|
&working(RepoKind::Jujutsu),
|
|
&working(RepoKind::Jujutsu),
|
|
ComparisonMode::Snapshots,
|
|
&[(root.join("file"), "frozen after\n".into())],
|
|
)
|
|
.unwrap();
|
|
fs::write(root.join("file"), "new checkout\n").unwrap();
|
|
jj(&root, &["new"]);
|
|
let refreshed = repository
|
|
.refresh_working_after(10, 11, &[])
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(refreshed.files.len(), 1);
|
|
let file = repository.load_file(11, 0).unwrap();
|
|
assert_eq!(file.before, ["base"]);
|
|
assert_eq!(file.after, ["new checkout"]);
|
|
let historical = revision(&base, RepoKind::Jujutsu);
|
|
repository
|
|
.compare(12, &historical, &historical, ComparisonMode::Snapshots, &[])
|
|
.unwrap();
|
|
jj(&root, &["new"]);
|
|
assert!(
|
|
repository
|
|
.refresh_working_after(12, 13, &[])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn branch_changes_resolves_local_and_private_remote_ancestry_without_active_mutation() {
|
|
let fixture = Fixture::new();
|
|
let active = fixture.dir("active\nquoted\"\\");
|
|
init_git(&active);
|
|
fs::write(active.join("common"), "base\n").unwrap();
|
|
git(&active, &["add", "."]);
|
|
git(&active, &["commit", "-m", "base"]);
|
|
let base = git(&active, &["rev-parse", "HEAD"]);
|
|
let source = fixture.0.join("source");
|
|
git(
|
|
&fixture.0,
|
|
&["clone", active.to_str().unwrap(), source.to_str().unwrap()],
|
|
);
|
|
git(&source, &["config", "user.name", "Diff Test"]);
|
|
git(&source, &["config", "user.email", "diff@example.invalid"]);
|
|
fs::write(source.join("remote-only"), "remote\n").unwrap();
|
|
git(&source, &["add", "."]);
|
|
git(&source, &["commit", "-m", "remote"]);
|
|
fs::write(active.join("local-only"), "local\n").unwrap();
|
|
git(&active, &["add", "."]);
|
|
git(&active, &["commit", "-m", "local"]);
|
|
let local = revision(&git(&active, &["rev-parse", "HEAD"]), RepoKind::Git);
|
|
git(
|
|
&active,
|
|
&["remote", "add", "origin", source.to_str().unwrap()],
|
|
);
|
|
let state = disk_state(&active);
|
|
let mut repository = open(&active);
|
|
let catalog = repository.list_remote("origin").unwrap();
|
|
let remote = catalog
|
|
.entries
|
|
.iter()
|
|
.find(|r| matches!(r.target, RevisionTarget::Remote { .. }))
|
|
.unwrap();
|
|
let branch = repository
|
|
.compare(40, &local, remote, ComparisonMode::BranchChanges, &[])
|
|
.unwrap();
|
|
assert_eq!(branch.files.len(), 1);
|
|
assert_eq!(branch.files[0].path, Path::new("remote-only"));
|
|
assert!(branch.before.ends_with(&base));
|
|
assert_eq!(repository.load_file(40, 0).unwrap().after, ["remote"]);
|
|
let reverse = repository
|
|
.compare(41, remote, &local, ComparisonMode::BranchChanges, &[])
|
|
.unwrap();
|
|
assert_eq!(reverse.files.len(), 1);
|
|
assert_eq!(reverse.files[0].path, Path::new("local-only"));
|
|
assert_eq!(repository.load_file(41, 0).unwrap().after, ["local"]);
|
|
assert!(
|
|
repository
|
|
.compare(42, &local, &local, ComparisonMode::BranchChanges, &[])
|
|
.unwrap()
|
|
.files
|
|
.is_empty()
|
|
);
|
|
assert_eq!(disk_state(&active), state);
|
|
}
|
|
|
|
#[test]
|
|
fn branch_changes_rejects_missing_and_multiple_merge_bases_without_snapshot_fallback() {
|
|
let fixture = Fixture::new();
|
|
let root = fixture.dir("graphs");
|
|
init_git(&root);
|
|
fs::write(root.join("file"), "base\n").unwrap();
|
|
git(&root, &["add", "."]);
|
|
git(&root, &["commit", "-m", "base"]);
|
|
let base = git(&root, &["rev-parse", "HEAD"]);
|
|
let tree = git(&root, &["rev-parse", "HEAD^{tree}"]);
|
|
let left = git(&root, &["commit-tree", &tree, "-p", &base, "-m", "left"]);
|
|
let right = git(&root, &["commit-tree", &tree, "-p", &base, "-m", "right"]);
|
|
let one = git(
|
|
&root,
|
|
&["commit-tree", &tree, "-p", &left, "-p", &right, "-m", "one"],
|
|
);
|
|
let two = git(
|
|
&root,
|
|
&["commit-tree", &tree, "-p", &right, "-p", &left, "-m", "two"],
|
|
);
|
|
let unrelated = git(&root, &["commit-tree", &tree, "-m", "unrelated"]);
|
|
let state = disk_state(&root);
|
|
let mut repository = open(&root);
|
|
for (before, after, message) in [
|
|
(&base, &unrelated, "no merge base"),
|
|
(&one, &two, "multiple merge bases"),
|
|
] {
|
|
let before = revision(before, RepoKind::Git);
|
|
let after = revision(after, RepoKind::Git);
|
|
assert!(
|
|
repository
|
|
.compare(50, &before, &after, ComparisonMode::BranchChanges, &[])
|
|
.unwrap_err()
|
|
.contains(message)
|
|
);
|
|
assert!(
|
|
repository
|
|
.compare(51, &before, &after, ComparisonMode::Snapshots, &[])
|
|
.unwrap()
|
|
.files
|
|
.is_empty()
|
|
);
|
|
}
|
|
assert_eq!(disk_state(&root), state);
|
|
}
|