mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
* perf(vcs): filter the Git log with git log instead of a background search index The log built a persistent inverted index of every commit's message, authors and changed paths in the background, with a progress bar above the commit list, and walked trees itself to filter by path. Building it took minutes on large repositories, and filters were slow until it finished. - Text: plain text uses `git log --fixed-strings --grep` (with `--regexp-ignore-case` unless Match Case is on); text of 4+ hex digits also matches hash prefixes. Regex filters still match in-process. - Paths: `git log -- :(literal)<paths>` replaces the in-process path walk and its per-commit tree cache. It uses git's history simplification and, when the repository has a changed-path commit-graph, its Bloom filters. Glob characters in a path are literal. - Authors match against the commit metadata cache. - Removed the search tables, `index_log`, the `vcs_log_index` command and the indexing progress bar. The commit metadata cache stays. * docs(contributing): ask for screenshots only when a change alters what users see Removing a feature together with its UI, copy-only and non-visual changes now describe what was checked instead of attaching before/after screenshots. * fix(vcs): match case-insensitive non-ASCII Git log text in process git folds case only for ASCII under the C locale it runs with, so a case-insensitive filter like "ärger" missed "Ärger". Such text is now matched against commit messages directly. Path and text selection also ignore log.showSignature and log.follow from the user's config.
465 lines
19 KiB
Rust
465 lines
19 KiB
Rust
use ide_vcs::{LogOrder, LogQuery, Repo};
|
|
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::process::{Command, Stdio};
|
|
|
|
fn git(dir: &Path, args: &[&str]) -> String {
|
|
let out = Command::new("git")
|
|
.current_dir(dir)
|
|
.args([
|
|
"-c",
|
|
"init.defaultBranch=main",
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"[email protected]",
|
|
"-c",
|
|
"commit.gpgsign=false",
|
|
])
|
|
.args(args)
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
|
String::from_utf8_lossy(&out.stdout).trim().to_string()
|
|
}
|
|
|
|
fn commit(dir: &Path, file: &str, message: &str) -> String {
|
|
std::fs::write(dir.join(file), message).unwrap();
|
|
git(dir, &["add", "-A"]);
|
|
git(dir, &["commit", "-qm", message]);
|
|
git(dir, &["rev-parse", "HEAD"])
|
|
}
|
|
|
|
#[test]
|
|
fn branch_and_independent_ranges_form_a_union() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let root = commit(dir, "a", "root");
|
|
let first = commit(dir, "a", "first");
|
|
let second = commit(dir, "a", "second");
|
|
let third = commit(dir, "a", "third");
|
|
git(dir, &["checkout", "-qb", "side", &root]);
|
|
let side = commit(dir, "b", "side");
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
for order in [LogOrder::Topo, LogOrder::Date, LogOrder::Standard] {
|
|
let log = repo
|
|
.log(&LogQuery {
|
|
branches: vec!["side".into()],
|
|
ranges: vec![format!("{root}..{first}"), format!("{second}..{third}")],
|
|
order,
|
|
..Default::default()
|
|
})
|
|
.unwrap();
|
|
let got: std::collections::HashSet<_> = log.iter().map(|c| c.id.clone()).collect();
|
|
assert_eq!(got, [root.clone(), first.clone(), third.clone(), side.clone()].into());
|
|
let ranges = repo
|
|
.log(&LogQuery {
|
|
ranges: vec![format!("{root}..{first}"), format!("{second}..{third}")],
|
|
order,
|
|
..Default::default()
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
ranges.iter().map(|c| c.id.clone()).collect::<std::collections::HashSet<_>>(),
|
|
[first.clone(), third.clone()].into()
|
|
);
|
|
let positions: std::collections::HashMap<_, _> =
|
|
log.iter().enumerate().map(|(i, c)| (c.id.as_str(), i)).collect();
|
|
for (i, c) in log.iter().enumerate() {
|
|
for p in c.graph_parents.as_ref().unwrap() {
|
|
assert!(positions.get(p.as_str()).is_none_or(|p| *p > i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn lazy_summaries_seek_and_index_survive_refresh_and_restart() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let mut ids = Vec::new();
|
|
for i in 0..70 {
|
|
ids.push(commit(dir, "a", &format!("summary {i}\n\nneedle body {i}")));
|
|
}
|
|
let cache = tempfile::tempdir().unwrap();
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
repo.enable_log_index(cache.path());
|
|
let query = LogQuery { summary_only: true, limit: 5, ..Default::default() };
|
|
let page = repo.log(&query).unwrap();
|
|
assert_eq!(page[0].message, "summary 69");
|
|
assert!(page[0].message_truncated);
|
|
assert_eq!(page[0].log_position, Some(0));
|
|
assert_eq!(page[0].log_total, Some(70));
|
|
let window = repo.log(&LogQuery { seek: Some(ids[5].clone()), limit: 20, ..query.clone() }).unwrap();
|
|
assert!(window.iter().any(|c| c.id == ids[5]));
|
|
assert!(window[0].log_position.unwrap() > 0);
|
|
assert!(repo.log(&LogQuery { request: Some(ide_core::RequestId(1)), ..query.clone() }).is_ok());
|
|
let cancel = ide_core::CancellationToken::new();
|
|
cancel.cancel();
|
|
assert!(repo.log_cancellable(&query, &cancel).is_err());
|
|
assert_eq!(repo.log(&LogQuery { text: Some("needle body 5".into()), ..query.clone() }).unwrap().len(), 5);
|
|
let new = commit(dir, "b", "new head");
|
|
let refreshed = repo.log(&LogQuery { retain_until: Some(ids[0].clone()), ..query.clone() }).unwrap();
|
|
assert_eq!(refreshed[0].id, new);
|
|
assert_eq!(refreshed.len(), 71);
|
|
drop(repo);
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
repo.enable_log_index(cache.path());
|
|
let hit = repo.log(&LogQuery { text: Some("needle body 69".into()), ..query }).unwrap();
|
|
assert_eq!(hit[0].id, ids[69]);
|
|
assert_eq!(repo.log_details(&ids[69]).unwrap().message, "summary 69\n\nneedle body 69");
|
|
assert_eq!(repo.log_details(&ids[69]).unwrap().signature, "N");
|
|
}
|
|
|
|
#[test]
|
|
fn followed_history_carries_the_filename_at_each_revision() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let first = commit(dir, "old.txt", "original");
|
|
git(dir, &["mv", "old.txt", "new.txt"]);
|
|
git(dir, &["commit", "-qm", "rename"]);
|
|
let renamed = git(dir, &["rev-parse", "HEAD"]);
|
|
let tip = commit(dir, "new.txt", "latest");
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let log = repo.log(&LogQuery { path: Some("new.txt".into()), follow: true, ..Default::default() }).unwrap();
|
|
assert_eq!(
|
|
log.iter().map(|c| (c.id.as_str(), c.history_path.as_deref())).collect::<Vec<_>>(),
|
|
vec![(tip.as_str(), Some("new.txt")), (renamed.as_str(), Some("new.txt")), (first.as_str(), Some("old.txt"))]
|
|
);
|
|
assert_eq!(repo.revision_children(&first, "").unwrap(), vec!["old.txt"]);
|
|
}
|
|
|
|
#[test]
|
|
fn detail_filters_connect_visible_ancestors_without_changing_revision_parents() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let a = commit(dir, "a", "keep root");
|
|
commit(dir, "b", "hidden middle");
|
|
git(dir, &["commit", "--amend", "--no-edit", "--author=Other <[email protected]>"]);
|
|
let b = git(dir, &["rev-parse", "HEAD"]);
|
|
let c = commit(dir, "a", "keep tip");
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
for q in [
|
|
LogQuery { text: Some("keep".into()), ..Default::default() },
|
|
LogQuery { path: Some("a".into()), ..Default::default() },
|
|
LogQuery { authors: vec!["Test".into()], authors_exact: true, ..Default::default() },
|
|
] {
|
|
let page = repo.log(&LogQuery { limit: 1, ..q.clone() }).unwrap();
|
|
assert_eq!(page[0].id, c);
|
|
assert_eq!(page[0].parents, vec![b.clone()]);
|
|
assert_eq!(page[0].graph_parents, Some(vec![a.clone()]));
|
|
assert_eq!(page[0].graph_dashed, vec![true]);
|
|
assert_eq!(page[0].on_current_branch, Some(true));
|
|
let next = repo.log(&LogQuery { skip: 1, limit: 1, ..q }).unwrap();
|
|
assert_eq!(next[0].id, a);
|
|
assert_eq!(next[0].graph_parents, Some(vec![]));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn all_history_includes_commits_reachable_only_from_tags() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let root = commit(dir, "a", "root");
|
|
let tip = commit(dir, "a", "tagged tip");
|
|
git(dir, &["tag", "v1"]);
|
|
git(dir, &["reset", "--hard", &root]);
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let log = repo.log(&LogQuery::default()).unwrap();
|
|
let tagged = log.iter().find(|c| c.id == tip).unwrap();
|
|
assert!(tagged.refs.contains(&"tag: v1".into()));
|
|
assert_eq!(tagged.on_current_branch, Some(false));
|
|
}
|
|
|
|
#[test]
|
|
fn projection_does_not_truncate_long_hidden_chains() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let mut stream = String::new();
|
|
for i in 0..4100 {
|
|
let message = if i == 0 || i == 4099 { "keep" } else { "hide" };
|
|
stream.push_str(&format!(
|
|
"commit refs/heads/main\ncommitter Test <[email protected]> {} +0000\ndata {}\n{}\n\n",
|
|
1000 + i,
|
|
message.len(),
|
|
message
|
|
));
|
|
}
|
|
let mut child = Command::new("git")
|
|
.current_dir(dir)
|
|
.args(["fast-import", "--quiet"])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::null())
|
|
.spawn()
|
|
.unwrap();
|
|
child.stdin.take().unwrap().write_all(stream.as_bytes()).unwrap();
|
|
assert!(child.wait().unwrap().success());
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let log = repo.log(&LogQuery { text: Some("keep".into()), ..Default::default() }).unwrap();
|
|
assert_eq!(log.len(), 2);
|
|
assert_eq!(log[0].graph_parents, Some(vec![log[1].id.clone()]));
|
|
}
|
|
|
|
#[test]
|
|
fn path_and_shallow_history_remain_topological_with_clock_skew() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let source = tmp.path().join("source");
|
|
std::fs::create_dir(&source).unwrap();
|
|
git(&source, &["init", "-q"]);
|
|
let make = |message: &str, parent: Option<&str>, time: &str| {
|
|
std::fs::write(source.join("a.txt"), message).unwrap();
|
|
git(&source, &["add", "a.txt"]);
|
|
let tree = git(&source, &["write-tree"]);
|
|
let mut cmd = Command::new("git");
|
|
cmd.current_dir(&source).args([
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"[email protected]",
|
|
"commit-tree",
|
|
&tree,
|
|
"-m",
|
|
message,
|
|
]);
|
|
if let Some(p) = parent {
|
|
cmd.args(["-p", p]);
|
|
}
|
|
let out = cmd.env("GIT_AUTHOR_DATE", time).env("GIT_COMMITTER_DATE", time).output().unwrap();
|
|
assert!(out.status.success());
|
|
String::from_utf8(out.stdout).unwrap().trim().to_string()
|
|
};
|
|
let root = make("root", None, "@5000 +0000");
|
|
let left = make("left", Some(&root), "@1000 +0000");
|
|
let right = make("right", Some(&root), "@2000 +0000");
|
|
git(&source, &["update-ref", "refs/heads/main", &left]);
|
|
git(&source, &["update-ref", "refs/heads/side", &right]);
|
|
let shallow = tmp.path().join("shallow");
|
|
git(
|
|
tmp.path(),
|
|
&[
|
|
"clone",
|
|
"-q",
|
|
"--depth=2",
|
|
"--no-single-branch",
|
|
&format!("file://{}", source.display()),
|
|
shallow.to_str().unwrap(),
|
|
],
|
|
);
|
|
for dir in [&source, &shallow] {
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
for order in [LogOrder::Date, LogOrder::Topo, LogOrder::Standard] {
|
|
let log = repo.log(&LogQuery { order, path: Some("a.txt".into()), ..Default::default() }).unwrap();
|
|
assert_eq!(log.len(), 3);
|
|
let positions: std::collections::HashMap<_, _> =
|
|
log.iter().enumerate().map(|(i, c)| (c.id.as_str(), i)).collect();
|
|
for (i, c) in log.iter().enumerate() {
|
|
for parent in c.graph_parents.as_ref().unwrap() {
|
|
assert!(positions.get(parent.as_str()).is_none_or(|p| *p > i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_paths_exact_authors_patterns_and_rename_history() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let root = commit(dir, "old.txt", "root");
|
|
git(dir, &["mv", "old.txt", "new.txt"]);
|
|
git(dir, &["commit", "-qm", "rename"]);
|
|
let rename = git(dir, &["rev-parse", "HEAD"]);
|
|
let tip = commit(dir, "other.txt", "another file");
|
|
git(dir, &["branch", "feature/log"]);
|
|
git(dir, &["branch", "feature/skip", &root]);
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let log = repo.log(&LogQuery { paths: vec!["new.txt".into(), "other.txt".into()], ..Default::default() }).unwrap();
|
|
assert_eq!(log.iter().map(|c| c.id.clone()).collect::<Vec<_>>(), vec![tip.clone(), rename.clone()]);
|
|
let log = repo.log(&LogQuery { path: Some("new.txt".into()), follow: true, ..Default::default() }).unwrap();
|
|
assert_eq!(log.iter().map(|c| c.id.clone()).collect::<Vec<_>>(), vec![rename, root.clone()]);
|
|
let log = repo
|
|
.log(&LogQuery { branches: vec!["feature/.*".into(), "-feature/skip".into()], ..Default::default() })
|
|
.unwrap();
|
|
assert_eq!(log[0].id, tip);
|
|
std::fs::write(dir.join("other.txt"), "different author").unwrap();
|
|
git(dir, &["add", "-A"]);
|
|
git(dir, &["-c", "user.name=Test User", "-c", "[email protected]", "commit", "-qm", "different author"]);
|
|
let log = repo.log(&LogQuery { authors: vec!["Test".into()], authors_exact: true, ..Default::default() }).unwrap();
|
|
assert_eq!(log.len(), 3);
|
|
assert!(log.iter().all(|c| c.author == "Test"));
|
|
assert!(repo.log_users().unwrap().contains(&"Test User <[email protected]>".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_and_path_cache_survive_restart_without_caching_refs_or_filters() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path().join("repo");
|
|
std::fs::create_dir(&dir).unwrap();
|
|
git(&dir, &["init", "-q"]);
|
|
let first = commit(&dir, "a", "first\n\nComplete commit body");
|
|
git(&dir, &["tag", "v1"]);
|
|
git(&dir, &["tag", "-a", "annotated", "-m", "release"]);
|
|
let cache = tmp.path().join("cache");
|
|
{
|
|
let repo = Repo::discover(&dir).unwrap().unwrap();
|
|
repo.enable_log_index(&cache);
|
|
let log = repo.log(&LogQuery { path: Some("a".into()), summary_only: true, ..Default::default() }).unwrap();
|
|
assert!(log[0].message_truncated);
|
|
assert_eq!(log[0].message, "first");
|
|
assert_eq!(repo.log_details(&first).unwrap().message, "first\n\nComplete commit body");
|
|
let details = repo.log_details("annotated").unwrap();
|
|
assert_eq!(details.message, "first\n\nComplete commit body");
|
|
assert_eq!(details.signature, "N");
|
|
assert!(details.containing_branches.contains(&"main".to_string()));
|
|
assert_eq!(repo.revision_files(&first).unwrap(), vec!["a"]);
|
|
}
|
|
let second = commit(&dir, "a", "second");
|
|
git(&dir, &["tag", "-f", "v1"]);
|
|
let repo = Repo::discover(&dir).unwrap().unwrap();
|
|
repo.enable_log_index(&cache);
|
|
let log = repo.log(&LogQuery { path: Some("a".into()), ..Default::default() }).unwrap();
|
|
assert_eq!(log[0].id, second);
|
|
assert!(log[0].refs.contains(&"tag: v1".into()));
|
|
assert!(!log[1].refs.contains(&"tag: v1".into()));
|
|
assert!(log[1].message.contains("Complete commit body"));
|
|
assert!(!log[1].message_truncated);
|
|
}
|
|
|
|
#[test]
|
|
fn cancelled_query_does_not_publish_a_partial_page() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
git(tmp.path(), &["init", "-q"]);
|
|
commit(tmp.path(), "a", "root");
|
|
let repo = Repo::discover(tmp.path()).unwrap().unwrap();
|
|
let cancel = ide_core::CancellationToken::new();
|
|
cancel.cancel();
|
|
assert_eq!(
|
|
repo.log_cancellable(&LogQuery::default(), &cancel).unwrap_err().kind,
|
|
ide_core::error::ErrorKind::Cancelled
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shallow_boundaries_do_not_reconnect_to_locally_present_ancestors() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let root = commit(dir, "a", "keep root");
|
|
let boundary = commit(dir, "a", "hidden boundary");
|
|
let tip = commit(dir, "a", "keep tip");
|
|
git(dir, &["tag", "root", &root]);
|
|
std::fs::write(dir.join(".git/shallow"), format!("{boundary}\n")).unwrap();
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
for order in [LogOrder::Date, LogOrder::Topo, LogOrder::Standard] {
|
|
for text in [None, Some("keep".to_string())] {
|
|
let log = repo.log(&LogQuery { order, text, path: Some("a".into()), ..Default::default() }).unwrap();
|
|
let boundary_row = log.iter().find(|c| c.id == boundary);
|
|
if let Some(row) = boundary_row {
|
|
assert_eq!(row.parents, vec![root.clone()]);
|
|
assert_eq!(row.graph_parents, Some(vec![]));
|
|
} else {
|
|
assert_eq!(log.iter().find(|c| c.id == tip).unwrap().graph_parents, Some(vec![]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exact_branch_exclusions_treat_punctuation_literally() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let root = commit(dir, "a", "root");
|
|
git(dir, &["branch", "release/1.0"]);
|
|
let tip = commit(dir, "a", "tip");
|
|
git(dir, &["branch", "release/1x0"]);
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let log = repo
|
|
.log(&LogQuery { branches: vec!["release/.*".into(), "-release/1.0".into()], ..Default::default() })
|
|
.unwrap();
|
|
assert_eq!(log.iter().map(|c| c.id.as_str()).collect::<Vec<_>>(), vec![tip.as_str(), root.as_str()]);
|
|
}
|
|
|
|
#[test]
|
|
fn mode_only_changes_appear_in_path_history() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let root = commit(dir, "script", "root");
|
|
git(dir, &["update-index", "--chmod=+x", "script"]);
|
|
git(dir, &["commit", "-qm", "make executable"]);
|
|
let tip = git(dir, &["rev-parse", "HEAD"]);
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let log = repo.log(&LogQuery { path: Some("script".into()), ..Default::default() }).unwrap();
|
|
assert_eq!(log.iter().map(|c| c.id.as_str()).collect::<Vec<_>>(), vec![tip.as_str(), root.as_str()]);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_paths_are_simplified_together_at_merges() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
commit(dir, "a", "a0");
|
|
commit(dir, "b", "b0");
|
|
git(dir, &["branch", "side"]);
|
|
commit(dir, "a", "left");
|
|
git(dir, &["checkout", "side"]);
|
|
commit(dir, "b", "right");
|
|
git(dir, &["checkout", "main"]);
|
|
git(dir, &["merge", "--no-ff", "side", "-m", "merge both paths"]);
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let expected = git(dir, &["log", "--topo-order", "--format=%H", "--", "a", "b"]);
|
|
for order in [LogOrder::Topo, LogOrder::Date, LogOrder::Standard] {
|
|
let log = repo
|
|
.log(&LogQuery {
|
|
branch: Some("HEAD".into()),
|
|
paths: vec!["a".into(), "b".into()],
|
|
order,
|
|
..Default::default()
|
|
})
|
|
.unwrap();
|
|
assert_eq!(log[0].summary, "merge both paths");
|
|
assert_eq!(
|
|
log.iter().map(|c| c.id.as_str()).collect::<std::collections::HashSet<_>>(),
|
|
expected.lines().collect()
|
|
);
|
|
assert_eq!(log[0].parents.len(), 2);
|
|
assert_eq!(log[0].graph_parents.as_ref().unwrap().len(), 2);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn paths_are_literal_and_text_matches_bodies_or_hash_prefixes() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let star = commit(dir, "[a]", "star\n\nBody Needle");
|
|
let plain = commit(dir, "a", "plain");
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let ids = |q: LogQuery| repo.log(&q).unwrap().into_iter().map(|c| c.id).collect::<Vec<_>>();
|
|
assert_eq!(ids(LogQuery { path: Some("[a]".into()), ..Default::default() }), vec![star.clone()]);
|
|
assert_eq!(ids(LogQuery { text: Some("body needle".into()), ..Default::default() }), vec![star.clone()]);
|
|
assert!(ids(LogQuery { text: Some("body needle".into()), text_match_case: true, ..Default::default() }).is_empty());
|
|
assert_eq!(ids(LogQuery { text: Some(plain[..7].to_uppercase()), ..Default::default() }), vec![plain]);
|
|
}
|
|
|
|
#[test]
|
|
fn case_insensitive_text_folds_non_ascii() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let dir = tmp.path();
|
|
git(dir, &["init", "-q"]);
|
|
let hit = commit(dir, "a", "Ärger ПРИВЕТ");
|
|
commit(dir, "a", "other");
|
|
let repo = Repo::discover(dir).unwrap().unwrap();
|
|
let ids = |q: LogQuery| repo.log(&q).unwrap().into_iter().map(|c| c.id).collect::<Vec<_>>();
|
|
assert_eq!(ids(LogQuery { text: Some("ärger привет".into()), ..Default::default() }), vec![hit.clone()]);
|
|
assert!(ids(LogQuery { text: Some("ärger".into()), text_match_case: true, ..Default::default() }).is_empty());
|
|
}
|