mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
File history passed one ^<sha>^@ exclusion per created file, and path-filtered log passed every selected path, as git arguments. A large module blew past Windows' ~32K command line (os error 206). Pass them through git log --stdin. Following renames only makes sense for a file: on a directory each created file cut the history short, leaving a single commit. Directories now use plain path filtering.
This commit is contained in:
3 files changed
+143
-16
No files matched your search
@@ -361,14 +361,48 @@ pub(crate) fn run_cancellable(root: &Path, args: &[&str], cancel: &CancellationT
|
||||
|
||||
/// Cancellable capture preserving exit codes for commands with documented non-error states.
|
||||
pub(crate) fn run_cancellable_raw(root: &Path, args: &[&str], cancel: &CancellationToken) -> IdeResult<Output> {
|
||||
run_cancellable_raw_stdin(root, args, None, cancel)
|
||||
}
|
||||
|
||||
/// Like `run_cancellable`, feeding `stdin` to git. Revisions and paths passed through
|
||||
/// `--stdin` are not bound by the command-line length limit (Windows: ~32K characters).
|
||||
pub(crate) fn run_cancellable_stdin(
|
||||
root: &Path,
|
||||
args: &[&str],
|
||||
stdin: &[u8],
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<Output> {
|
||||
let out = run_cancellable_raw_stdin(root, args, Some(stdin), cancel)?;
|
||||
if out.code == Some(0) { Ok(out) } else { Err(failure(args, &out.stderr, &out.stdout, out.code)) }
|
||||
}
|
||||
|
||||
fn run_cancellable_raw_stdin(
|
||||
root: &Path,
|
||||
args: &[&str],
|
||||
stdin: Option<&[u8]>,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<Output> {
|
||||
if cancel.is_cancelled() {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
let mut cmd = command(root)?;
|
||||
cmd.args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
cmd.args(args)
|
||||
.stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() })
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
own_group(&mut cmd);
|
||||
let mut session = CommandSession::start(&mut cmd);
|
||||
let mut child = cmd.spawn().map_err(spawn_error)?;
|
||||
let writer = match (stdin, child.stdin.take()) {
|
||||
(Some(data), Some(mut pipe)) => {
|
||||
let data = data.to_vec();
|
||||
Some(std::thread::spawn(move || {
|
||||
// A broken pipe just means git stopped reading early; its exit status tells the story.
|
||||
let _ = pipe.write_all(&data);
|
||||
}))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let capture = |mut pipe: Box<dyn Read + Send>| {
|
||||
std::thread::spawn(move || {
|
||||
let mut bytes = Vec::new();
|
||||
@@ -382,6 +416,9 @@ pub(crate) fn run_cancellable_raw(root: &Path, args: &[&str], cancel: &Cancellat
|
||||
terminate_tree(&mut child, KILL_GRACE);
|
||||
let _ = stdout.join();
|
||||
let _ = stderr.join();
|
||||
if let Some(w) = writer {
|
||||
let _ = w.join();
|
||||
}
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
if let Some(status) = child.try_wait()? {
|
||||
@@ -389,6 +426,9 @@ pub(crate) fn run_cancellable_raw(root: &Path, args: &[&str], cancel: &Cancellat
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
};
|
||||
if let Some(w) = writer {
|
||||
let _ = w.join();
|
||||
}
|
||||
let join = |thread: std::thread::JoinHandle<std::io::Result<Vec<u8>>>| {
|
||||
thread.join().map_err(|_| IdeError::internal("git output reader panicked"))?.map_err(IdeError::from)
|
||||
};
|
||||
|
||||
@@ -80,12 +80,18 @@ fn git_select(
|
||||
args.push("--first-parent".into());
|
||||
}
|
||||
args.extend(filter.iter().cloned());
|
||||
// Past this many bytes of paths the command line risks Windows' ~32K limit.
|
||||
let stdin = (paths.iter().map(|p| p.len() + 12).sum::<usize>() > 8 * 1024).then(|| paths_stdin(paths));
|
||||
if stdin.is_some() {
|
||||
args.push("--stdin".into());
|
||||
}
|
||||
args.push("--end-of-options".into());
|
||||
args.extend(scope.split_whitespace().map(str::to_string));
|
||||
args.push("--".into());
|
||||
args.extend(paths.iter().map(|p| format!(":(literal){p}")));
|
||||
let output =
|
||||
crate::cli::run_cancellable(r.root(), &args.iter().map(String::as_str).collect::<Vec<_>>(), cancel)?;
|
||||
if stdin.is_none() {
|
||||
args.push("--".into());
|
||||
args.extend(paths.iter().map(|p| format!(":(literal){p}")));
|
||||
}
|
||||
let output = run_log(r, args, stdin.as_deref(), cancel)?;
|
||||
for line in output.stdout_str().lines() {
|
||||
ids.insert(ObjectId::from_hex(line.as_bytes()).map_err(gx)?);
|
||||
}
|
||||
@@ -93,6 +99,28 @@ fn git_select(
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// Stdin for `git log --stdin` that limits the walk to `paths`, however many there are.
|
||||
fn paths_stdin(paths: &[String]) -> String {
|
||||
let mut text = String::from("--\n");
|
||||
for p in paths {
|
||||
text.push_str(&format!(":(literal){p}\n"));
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn run_log(
|
||||
r: &Repo,
|
||||
args: Vec<String>,
|
||||
stdin: Option<&str>,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<crate::cli::Output> {
|
||||
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||
match stdin {
|
||||
Some(text) => crate::cli::run_cancellable_stdin(r.root(), &argv, text.as_bytes(), cancel),
|
||||
None => crate::cli::run_cancellable(r.root(), &argv, cancel),
|
||||
}
|
||||
}
|
||||
|
||||
/// The commits of a file's history and the path it had in each. The commits that changed
|
||||
/// the path, merges included (`--full-history --simplify-merges`); where it appears, a
|
||||
/// rename from any parent continues the history under the old path from that parent.
|
||||
@@ -114,21 +142,27 @@ fn file_history(
|
||||
if !started.insert((revs.clone(), path.clone())) {
|
||||
continue;
|
||||
}
|
||||
let mut args: Vec<String> =
|
||||
let mut opts: Vec<String> =
|
||||
["log", "--format=%x00%H", "--name-status", "-z", "--full-history", "--simplify-merges", "--no-follow"]
|
||||
.map(String::from)
|
||||
.into();
|
||||
if first_parent {
|
||||
args.push("--first-parent".into());
|
||||
opts.push("--first-parent".into());
|
||||
}
|
||||
args.push("--end-of-options".into());
|
||||
args.extend(revs);
|
||||
let pathspec = ["--".to_string(), format!(":(literal){path}")];
|
||||
let run = |args: &[String]| -> IdeResult<Vec<(ObjectId, Option<char>)>> {
|
||||
let argv: Vec<&str> = args.iter().chain(&pathspec).map(String::as_str).collect();
|
||||
Ok(parse_name_status_log(&crate::cli::run_cancellable(r.root(), &argv, cancel)?.stdout_str()))
|
||||
// `excluded` can name a revision per created file, so it goes through stdin.
|
||||
let run = |excluded: &[String]| -> IdeResult<Vec<(ObjectId, Option<char>)>> {
|
||||
let mut args = opts.clone();
|
||||
if !excluded.is_empty() {
|
||||
args.push("--stdin".into());
|
||||
}
|
||||
args.push("--end-of-options".into());
|
||||
args.extend(revs.iter().cloned());
|
||||
args.extend(pathspec.iter().cloned());
|
||||
let stdin = (!excluded.is_empty()).then(|| excluded.join("\n") + "\n");
|
||||
Ok(parse_name_status_log(&run_log(r, args, stdin.as_deref(), cancel)?.stdout_str()))
|
||||
};
|
||||
let records = run(&args)?;
|
||||
let records = run(&[])?;
|
||||
// Where the file appears its history ends: what is behind that is another file of
|
||||
// the same name (a rename continues the history below). The commits of the history
|
||||
// are those not behind a creation; the statuses come from the full listing (with
|
||||
@@ -137,8 +171,8 @@ fn file_history(
|
||||
let kept: Option<HashSet<ObjectId>> = if created.is_empty() {
|
||||
None
|
||||
} else {
|
||||
args.extend(created.iter().map(|id| format!("^{id}^@")));
|
||||
Some(run(&args)?.into_iter().map(|(id, _)| id).collect())
|
||||
let excluded: Vec<String> = created.iter().map(|id| format!("^{id}^@")).collect();
|
||||
Some(run(&excluded)?.into_iter().map(|(id, _)| id).collect())
|
||||
};
|
||||
let mut created = Vec::new();
|
||||
for (id, first_status) in records {
|
||||
@@ -354,7 +388,9 @@ fn log_indexed(
|
||||
let mut history_paths = HashMap::new();
|
||||
let path_ids = if paths.is_empty() {
|
||||
None
|
||||
} else if q.follow && paths.len() == 1 {
|
||||
} else if q.follow && paths.len() == 1 && !r.root().join(&paths[0]).is_dir() {
|
||||
// Following renames is for a file: a directory gains files all the time, and each
|
||||
// creation would cut its history short.
|
||||
let (ids, at) = file_history(r, &scopes, &paths[0], q.first_parent, cancel)?;
|
||||
history_paths = at;
|
||||
Some(ids)
|
||||
|
||||
@@ -649,3 +649,54 @@ fn file_history_through_a_cyclic_rename() {
|
||||
pairs(&[("b to a", "a.txt"), ("Modified b", "b.txt"), ("a to b", "b.txt"), ("Created a", "a.txt")])
|
||||
);
|
||||
}
|
||||
|
||||
/// One commit per file under `dir/`, made with fast-import so hundreds stay quick.
|
||||
fn commit_files(repo_dir: &Path, count: usize, name: impl Fn(usize) -> String) {
|
||||
let mut stream = String::new();
|
||||
for i in 0..count {
|
||||
let path = name(i);
|
||||
stream.push_str(&format!(
|
||||
"commit refs/heads/main\ncommitter T <[email protected]> {} +0000\ndata 3\nc{}\n",
|
||||
1_700_000_000 + i,
|
||||
i % 10
|
||||
));
|
||||
stream.push_str(&format!("M 100644 inline {path}\ndata 1\nx\n\n"));
|
||||
}
|
||||
let mut child = Command::new("git")
|
||||
.current_dir(repo_dir)
|
||||
.args(["fast-import", "--quiet"])
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
child.stdin.take().unwrap().write_all(stream.as_bytes()).unwrap();
|
||||
assert!(child.wait().unwrap().success());
|
||||
git(repo_dir, &["symbolic-ref", "HEAD", "refs/heads/main"]);
|
||||
git(repo_dir, &["reset", "-q", "--hard"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn followed_directory_history_survives_hundreds_of_created_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dir = tmp.path();
|
||||
git(dir, &["init", "-q"]);
|
||||
// Every created file adds a `^<sha>^@` exclusion: ~800 of them passed as arguments exceed
|
||||
// Windows' command-line limit (os error 206).
|
||||
commit_files(dir, 800, |i| format!("module/src/main/java/com/example/generated/Class{i:04}.java"));
|
||||
let repo = Repo::discover(dir).unwrap().unwrap();
|
||||
let log = repo.log(&LogQuery { path: Some("module".into()), follow: true, ..Default::default() }).unwrap();
|
||||
assert_eq!(log.len(), 800);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_selected_paths_filter_history_without_a_huge_command_line() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dir = tmp.path();
|
||||
git(dir, &["init", "-q"]);
|
||||
commit_files(dir, 600, |i| format!("module/src/main/java/com/example/generated/Class{i:04}.java"));
|
||||
let repo = Repo::discover(dir).unwrap().unwrap();
|
||||
let paths: Vec<String> =
|
||||
(0..500).map(|i| format!("module/src/main/java/com/example/generated/Class{i:04}.java")).collect();
|
||||
let paths = paths.into_iter().map(Into::into).collect();
|
||||
let log = repo.log(&LogQuery { paths, text: Some("c3".into()), ..Default::default() }).unwrap();
|
||||
assert_eq!(log.len(), 50);
|
||||
}
|
||||
Reference in new issue
Block a user