1use std::collections::{BTreeMap, HashMap};
7use std::ffi::OsString;
8use std::fs;
9use std::io::Write;
10use std::ops::Deref;
11use std::path::{Path, PathBuf};
12use std::process::{Command, Stdio};
13
14use crate::core::config::env_vars::{
15 TEST_GIT, TEST_SEPARATE_COMMAND_BINARIES, get_git_exec_path, get_path_to_git,
16 should_use_separate_command_binary,
17};
18use crate::git::{GitRunInfo, GitVersion, NonZeroOid, Repo};
19use crate::util::get_sh;
20use color_eyre::Help;
21use eyre::Context;
22use itertools::Itertools;
23use lazy_static::lazy_static;
24use once_cell::sync::OnceCell;
25use regex::{Captures, Regex};
26use tempfile::TempDir;
27use tracing::{instrument, warn};
28
29fn find_cargo_target_profile_dir() -> Option<PathBuf> {
32 let current_exe = std::env::current_exe().ok()?;
33 let maybe_deps = current_exe.parent()?;
34 if maybe_deps.file_name()? == "deps" {
37 Some(maybe_deps.parent()?.to_path_buf())
38 } else {
39 Some(maybe_deps.to_path_buf())
40 }
41}
42
43fn try_find_cargo_bin(name: &str) -> Option<PathBuf> {
46 let bin_path =
47 find_cargo_target_profile_dir()?.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
48 bin_path.exists().then_some(bin_path)
49}
50
51const DUMMY_NAME: &str = "Testy McTestface";
52const DUMMY_EMAIL: &str = "test@example.com";
53const DUMMY_DATE: &str = "Wed 29 Oct 12:34:56 2020 PDT";
54
55#[derive(Clone, Debug)]
57pub struct Git {
58 pub repo_path: PathBuf,
62
63 pub path_to_git: PathBuf,
66
67 pub git_exec_path: PathBuf,
69}
70
71#[derive(Debug)]
73pub struct GitInitOptions {
74 pub make_initial_commit: bool,
77
78 pub run_branchless_init: bool,
80}
81
82impl Default for GitInitOptions {
83 fn default() -> Self {
84 GitInitOptions {
85 make_initial_commit: true,
86 run_branchless_init: true,
87 }
88 }
89}
90
91#[derive(Debug, Default)]
93pub struct GitRunOptions {
94 pub time: isize,
97
98 pub expected_exit_code: i32,
100
101 pub input: Option<String>,
103
104 pub env: HashMap<String, String>,
106
107 pub subdir: Option<PathBuf>,
109}
110
111impl Git {
112 pub fn new(path_to_git: PathBuf, repo_path: PathBuf, git_exec_path: PathBuf) -> Self {
114 Git {
115 repo_path,
116 path_to_git,
117 git_exec_path,
118 }
119 }
120
121 pub fn preprocess_output(&self, stdout: String) -> eyre::Result<String> {
123 let path_to_git = self
124 .path_to_git
125 .to_str()
126 .ok_or_else(|| eyre::eyre!("Could not convert path to Git to string"))?;
127 let output = stdout.replace(path_to_git, "<git-executable>");
128
129 let repo_path = std::fs::canonicalize(&self.repo_path)?;
132
133 let repo_path = repo_path
134 .to_str()
135 .ok_or_else(|| eyre::eyre!("Could not convert repo path to string"))?;
136 let output = output.replace(repo_path, "<repo-path>");
137
138 lazy_static! {
139 static ref CLEAR_LINE_RE: Regex = Regex::new(r"(^|\n).*(\r|\x1B\[K)").unwrap();
149 }
150 let output = CLEAR_LINE_RE
151 .replace_all(&output, |captures: &Captures| {
152 captures[1].to_string()
154 })
155 .into_owned();
156
157 lazy_static! {
158 static ref WHITESPACE_ONLY_LINE_RE: Regex = Regex::new(r"(?m)^ {4}$").unwrap();
171 }
172 let output = WHITESPACE_ONLY_LINE_RE
173 .replace_all(&output, "")
174 .into_owned();
175
176 Ok(output)
177 }
178
179 pub fn get_path_for_env(&self) -> OsString {
181 let branchless_path = find_cargo_target_profile_dir()
182 .expect("Unable to find git-branchless target directory");
183 let bash = get_sh().expect("bash missing?");
184 let bash_path = bash.parent().unwrap();
185 std::env::join_paths(vec![
186 branchless_path.as_os_str(),
188 self.git_exec_path.as_os_str(),
190 bash_path.as_os_str(),
192 ])
193 .expect("joining paths")
194 }
195
196 pub fn get_base_env(&self, time: isize) -> Vec<(OsString, OsString)> {
198 let date: OsString = format!("{DUMMY_DATE} -{time:0>2}").into();
201
202 let git_editor = OsString::from(":");
209
210 let new_path = self.get_path_for_env();
211 let envs = vec![
212 ("GIT_CONFIG_NOSYSTEM", OsString::from("1")),
213 ("GIT_AUTHOR_DATE", date.clone()),
214 ("GIT_COMMITTER_DATE", date),
215 ("GIT_EDITOR", git_editor),
216 ("GIT_EXEC_PATH", self.git_exec_path.as_os_str().into()),
217 ("LC_ALL", "C".into()),
218 ("PATH", new_path),
219 (TEST_GIT, self.path_to_git.as_os_str().into()),
220 (
221 TEST_SEPARATE_COMMAND_BINARIES,
222 std::env::var_os(TEST_SEPARATE_COMMAND_BINARIES).unwrap_or_default(),
223 ),
224 ];
225
226 envs.into_iter()
227 .map(|(key, value)| (OsString::from(key), value))
228 .collect()
229 }
230
231 #[track_caller]
232 #[instrument]
233 fn run_with_options_inner(
234 &self,
235 args: &[&str],
236 options: &GitRunOptions,
237 ) -> eyre::Result<(String, String)> {
238 let GitRunOptions {
239 time,
240 expected_exit_code,
241 input,
242 env,
243 subdir,
244 } = options;
245
246 let current_dir = subdir.as_ref().map_or(self.repo_path.clone(), |subdir| {
247 let mut p = self.repo_path.clone();
248 p.push(subdir);
249 p
250 });
251 let env: BTreeMap<_, _> = self
252 .get_base_env(*time)
253 .into_iter()
254 .chain(
255 env.iter()
256 .map(|(k, v)| (OsString::from(k), OsString::from(v))),
257 )
258 .collect();
259 let mut command = Command::new(&self.path_to_git);
260 command
261 .current_dir(¤t_dir)
262 .args(args)
263 .env_clear()
264 .envs(&env);
265
266 let result = if let Some(input) = input {
267 let mut child = command
268 .stdin(Stdio::piped())
269 .stdout(Stdio::piped())
270 .stderr(Stdio::piped())
271 .spawn()?;
272 write!(child.stdin.take().unwrap(), "{}", &input)?;
273 child.wait_with_output().wrap_err_with(|| {
274 format!(
275 "Running git
276 Executable: {:?}
277 Args: {:?}
278 Stdin: {:?}
279 Env: <not shown>",
280 &self.path_to_git, &args, input
281 )
282 })?
283 } else {
284 command.output().wrap_err_with(|| {
285 format!(
286 "Running git
287 Executable: {:?}
288 Args: {:?}
289 Env: <not shown>",
290 &self.path_to_git, &args
291 )
292 })?
293 };
294
295 let exit_code = result
296 .status
297 .code()
298 .expect("Failed to read exit code from Git process");
299 let result = if exit_code != *expected_exit_code {
300 eyre::bail!(
301 "Git command {:?} {:?} exited with unexpected code {} (expected {})
302env:
303{:#?}
304stdout:
305{}
306stderr:
307{}",
308 &self.path_to_git,
309 &args,
310 exit_code,
311 expected_exit_code,
312 &env,
313 &String::from_utf8_lossy(&result.stdout),
314 &String::from_utf8_lossy(&result.stderr),
315 )
316 } else {
317 result
318 };
319 let stdout = String::from_utf8(result.stdout)?;
320 let stdout = self.preprocess_output(stdout)?;
321 let stderr = String::from_utf8(result.stderr)?;
322 let stderr = self.preprocess_output(stderr)?;
323 Ok((stdout, stderr))
324 }
325
326 #[track_caller]
328 pub fn run_with_options<S: AsRef<str> + std::fmt::Debug>(
329 &self,
330 args: &[S],
331 options: &GitRunOptions,
332 ) -> eyre::Result<(String, String)> {
333 self.run_with_options_inner(
334 args.iter().map(|arg| arg.as_ref()).collect_vec().as_slice(),
335 options,
336 )
337 }
338
339 #[track_caller]
341 pub fn run<S: AsRef<str> + std::fmt::Debug>(
342 &self,
343 args: &[S],
344 ) -> eyre::Result<(String, String)> {
345 if let Some(first_arg) = args.first() {
346 if first_arg.as_ref() == "branchless" {
347 eyre::bail!(
348 r#"Refusing to invoke `branchless` via `git.run(&["branchless", ...])`; instead, call `git.branchless(&[...])`"#
349 );
350 }
351 }
352
353 self.run_with_options(args, &Default::default())
354 }
355
356 #[instrument]
358 pub fn smartlog(&self) -> eyre::Result<String> {
359 let (stdout, _stderr) = self.branchless("smartlog", &[])?;
360 Ok(stdout)
361 }
362
363 #[track_caller]
366 #[instrument]
367 pub fn branchless(&self, subcommand: &str, args: &[&str]) -> eyre::Result<(String, String)> {
368 self.branchless_with_options(subcommand, args, &Default::default())
369 }
370
371 #[track_caller]
375 #[instrument]
376 pub fn branchless_with_options(
377 &self,
378 subcommand: &str,
379 args: &[&str],
380 options: &GitRunOptions,
381 ) -> eyre::Result<(String, String)> {
382 let mut git_run_args = Vec::new();
383 if should_use_separate_command_binary(subcommand) {
384 git_run_args.push(format!("branchless-{subcommand}"));
385 } else {
386 git_run_args.push("branchless".to_string());
387 git_run_args.push(subcommand.to_string());
388 }
389 git_run_args.extend(args.iter().map(|arg| arg.to_string()));
390
391 let result = self.run_with_options(&git_run_args, options);
392
393 if !should_use_separate_command_binary(subcommand) {
394 let main_command_exe = try_find_cargo_bin("git-branchless");
395 let subcommand_exe = try_find_cargo_bin(&format!("git-branchless-{subcommand}"));
396 if let (Some(main_command_exe), Some(subcommand_exe)) =
397 (main_command_exe, subcommand_exe)
398 {
399 let main_command_mtime = main_command_exe.metadata()?.modified()?;
400 let subcommand_mtime = subcommand_exe.metadata()?.modified()?;
401 if subcommand_mtime > main_command_mtime {
402 result.suggestion(format!(
403 "\
404The modified time for {main_command_exe:?} was before the modified time for
405{subcommand_exe:?}, which may indicate that you made changes to the subcommand
406without building the main executable. This may cause spurious test failures
407because the main executable code is out of date.
408
409If so, you should either explicitly run: cargo -p git-branchless
410to build the main executable before running this test; or, if it's okay to skip
411building the main executable and test only the subcommand executable, you
412can set the environment variable
413`{TEST_SEPARATE_COMMAND_BINARIES}={subcommand}` to directly invoke it.\
414"
415 ))
416 } else {
417 result
418 }
419 } else {
420 result
421 }
422 } else {
423 result.suggestion(format!(
424 "\
425If you have set the {TEST_SEPARATE_COMMAND_BINARIES} environment variable, then \
426the git-branchless-{subcommand} binary is NOT automatically built or updated when \
427running integration tests for other binaries (see \
428https://github.com/rust-lang/cargo/issues/4316 for more details).
429
430Make sure that git-branchless-{subcommand} has been built before running \
431integration tests. You can build it with: cargo build -p git-branchless-{subcommand}
432
433If you have not set the {TEST_SEPARATE_COMMAND_BINARIES} environment variable, \
434then you can only run tests in the main `git-branchless` and \
435`git-branchless-lib` crates.\
436 ",
437 ))
438 }
439 }
440
441 #[instrument]
444 pub fn init_repo_with_options(&self, options: &GitInitOptions) -> eyre::Result<()> {
445 self.run(&["init"])?;
446 self.run(&["config", "user.name", DUMMY_NAME])?;
447 self.run(&["config", "user.email", DUMMY_EMAIL])?;
448 self.run(&["config", "core.abbrev", "7"])?;
449
450 if options.make_initial_commit {
451 self.commit_file("initial", 0)?;
452 }
453
454 self.run(&[
456 "config",
457 "branchless.commitDescriptors.relativeTime",
458 "false",
459 ])?;
460 self.run(&["config", "branchless.restack.preserveTimestamps", "true"])?;
461
462 self.run(&["config", "core.autocrlf", "false"])?;
469
470 if options.run_branchless_init {
471 self.branchless("init", &[])?;
472 }
473
474 Ok(())
475 }
476
477 pub fn init_repo(&self) -> eyre::Result<()> {
480 self.init_repo_with_options(&Default::default())
481 }
482
483 pub fn clone_repo_into(&self, target: &Git, additional_args: &[&str]) -> eyre::Result<()> {
486 let remote = format!("file://{}", self.repo_path.to_str().unwrap());
487 let args = {
488 let mut args = vec![
489 "clone",
490 "-c",
492 "core.autocrlf=false",
493 &remote,
494 target.repo_path.to_str().unwrap(),
495 ];
496 args.extend(additional_args.iter());
497 args
498 };
499
500 let (_stdout, _stderr) = self.run(args.as_slice())?;
501
502 let new_repo = Git {
505 repo_path: target.repo_path.clone(),
506 ..self.clone()
507 };
508 new_repo.init_repo_with_options(&GitInitOptions {
509 make_initial_commit: false,
510 run_branchless_init: false,
511 })?;
512
513 Ok(())
514 }
515
516 pub fn write_file_txt(&self, name: &str, contents: &str) -> eyre::Result<()> {
520 let name = format!("{name}.txt");
521 self.write_file(&name, contents)
522 }
523
524 pub fn write_file(&self, name: &str, contents: &str) -> eyre::Result<()> {
526 let path = self.repo_path.join(name);
527 if let Some(dir) = path.parent() {
528 std::fs::create_dir_all(self.repo_path.join(dir))?;
529 }
530 std::fs::write(&path, contents)?;
531 Ok(())
532 }
533
534 pub fn delete_file(&self, name: &str) -> eyre::Result<()> {
536 let file_path = self.repo_path.join(format!("{name}.txt"));
537 fs::remove_file(file_path)?;
538 Ok(())
539 }
540
541 pub fn set_file_permissions(
543 &self,
544 name: &str,
545 permissions: fs::Permissions,
546 ) -> eyre::Result<()> {
547 let file_path = self.repo_path.join(format!("{name}.txt"));
548 fs::set_permissions(file_path, permissions)?;
549 Ok(())
550 }
551
552 #[instrument]
555 pub fn get_trimmed_diff(&self, file: &str, commit: &str) -> eyre::Result<String> {
556 let (stdout, _stderr) = self.run(&["show", "--pretty=format:", commit])?;
557 let split_on = format!("+++ b/{file}\n");
558 match stdout.as_str().split_once(split_on.as_str()) {
559 Some((_, diff)) => Ok(diff.to_string()),
560 None => eyre::bail!("Error trimming diff. Could not split on '{split_on}'"),
561 }
562 }
563
564 #[track_caller]
568 #[instrument]
569 pub fn commit_file_with_contents_and_message(
570 &self,
571 name: &str,
572 time: isize,
573 contents: &str,
574 message_prefix: &str,
575 ) -> eyre::Result<NonZeroOid> {
576 let message = format!("{message_prefix} {name}.txt");
577 self.write_file_txt(name, contents)?;
578 self.run(&["add", "."])?;
579 self.run_with_options(
580 &["commit", "-m", &message],
581 &GitRunOptions {
582 time,
583 ..Default::default()
584 },
585 )?;
586
587 let repo = self.get_repo()?;
588 let oid = repo
589 .get_head_info()?
590 .oid
591 .expect("Could not find OID for just-created commit");
592 Ok(oid)
593 }
594
595 #[track_caller]
599 #[instrument]
600 pub fn commit_file_with_contents(
601 &self,
602 name: &str,
603 time: isize,
604 contents: &str,
605 ) -> eyre::Result<NonZeroOid> {
606 self.commit_file_with_contents_and_message(name, time, contents, "create")
607 }
608
609 #[track_caller]
612 #[instrument]
613 pub fn commit_file(&self, name: &str, time: isize) -> eyre::Result<NonZeroOid> {
614 self.commit_file_with_contents(name, time, &format!("{name} contents\n"))
615 }
616
617 #[instrument]
621 pub fn detach_head(&self) -> eyre::Result<()> {
622 self.run(&["checkout", "--detach"])?;
623 Ok(())
624 }
625
626 #[instrument]
628 pub fn get_repo(&self) -> eyre::Result<Repo> {
629 let repo = Repo::from_dir(&self.repo_path)?;
630 Ok(repo)
631 }
632
633 #[instrument]
635 pub fn get_version(&self) -> eyre::Result<GitVersion> {
636 let (version_str, _stderr) = self.run(&["version"])?;
637 let version = version_str.parse()?;
638 Ok(version)
639 }
640
641 #[instrument]
643 pub fn get_git_run_info(&self) -> GitRunInfo {
644 GitRunInfo {
645 path_to_git: self.path_to_git.clone(),
646 working_directory: self.repo_path.clone(),
647 env: self.get_base_env(0).into_iter().collect(),
648 }
649 }
650
651 #[instrument]
654 pub fn supports_reference_transactions(&self) -> eyre::Result<bool> {
655 let version = self.get_version()?;
656 Ok(version >= GitVersion(2, 29, 0))
657 }
658
659 pub fn supports_committer_date_is_author_date(&self) -> eyre::Result<bool> {
665 let version = self.get_version()?;
682 Ok(version >= GitVersion(2, 29, 0))
683 }
684
685 pub fn supports_log_exclude_decoration(&self) -> eyre::Result<bool> {
688 let version = self.get_version()?;
689 Ok(version >= GitVersion(2, 27, 0))
690 }
691
692 pub fn produces_auto_merge_refs(&self) -> eyre::Result<bool> {
695 let version = self.get_version()?;
696 Ok(version >= GitVersion(2, 44, 0))
697 }
698
699 #[instrument]
702 pub fn resolve_file(&self, name: &str, contents: &str) -> eyre::Result<()> {
703 let file_path = self.repo_path.join(format!("{name}.txt"));
704 std::fs::write(&file_path, contents)?;
705 let file_path = match file_path.to_str() {
706 None => eyre::bail!("Could not convert file path to string: {:?}", file_path),
707 Some(file_path) => file_path,
708 };
709 self.run(&["add", file_path])?;
710 Ok(())
711 }
712
713 #[instrument]
717 pub fn clear_event_log(&self) -> eyre::Result<()> {
718 let event_log_path = self.repo_path.join(".git/branchless/db.sqlite3");
719 std::fs::remove_file(event_log_path)?;
720 Ok(())
721 }
722}
723
724pub struct GitWrapper {
726 repo_dir: TempDir,
727 git: Git,
728}
729
730impl Deref for GitWrapper {
731 type Target = Git;
732
733 fn deref(&self) -> &Self::Target {
734 &self.git
735 }
736}
737
738fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
741 fs::create_dir_all(&dst)?;
742 for entry in fs::read_dir(src)? {
743 let entry = entry?;
744 let ty = entry.file_type()?;
745 if ty.is_dir() {
746 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
747 } else {
748 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
749 }
750 }
751 Ok(())
752}
753
754impl GitWrapper {
755 pub fn duplicate_repo(&self) -> eyre::Result<Self> {
761 let repo_dir = tempfile::tempdir()?;
762 copy_dir_all(&self.repo_dir, &repo_dir)?;
763 let git = Git {
764 repo_path: repo_dir.path().to_path_buf(),
765 ..self.git.clone()
766 };
767 Ok(Self { repo_dir, git })
768 }
769}
770
771static COLOR_EYRE_INSTALL: OnceCell<()> = OnceCell::new();
772
773pub fn make_git() -> eyre::Result<GitWrapper> {
775 COLOR_EYRE_INSTALL.get_or_try_init(color_eyre::install)?;
776
777 let repo_dir = tempfile::tempdir()?;
778 let path_to_git = get_path_to_git()?;
779 let git_exec_path = get_git_exec_path()?;
780 let git = Git::new(path_to_git, repo_dir.path().to_path_buf(), git_exec_path);
781 Ok(GitWrapper { repo_dir, git })
782}
783
784pub struct GitWrapperWithRemoteRepo {
788 pub temp_dir: TempDir,
791
792 pub original_repo: Git,
794
795 pub cloned_repo: Git,
797}
798
799pub fn make_git_with_remote_repo() -> eyre::Result<GitWrapperWithRemoteRepo> {
801 let path_to_git = get_path_to_git()?;
802 let git_exec_path = get_git_exec_path()?;
803 let temp_dir = tempfile::tempdir()?;
804 let original_repo_path = temp_dir.path().join("original");
805 std::fs::create_dir_all(&original_repo_path)?;
806 let original_repo = Git::new(
807 path_to_git.clone(),
808 original_repo_path,
809 git_exec_path.clone(),
810 );
811 let cloned_repo_path = temp_dir.path().join("cloned");
812 let cloned_repo = Git::new(path_to_git, cloned_repo_path, git_exec_path);
813
814 Ok(GitWrapperWithRemoteRepo {
815 temp_dir,
816 original_repo,
817 cloned_repo,
818 })
819}
820
821pub struct GitWorktreeWrapper {
823 pub temp_dir: TempDir,
826
827 pub worktree: Git,
829}
830
831pub fn make_git_worktree(git: &Git, worktree_name: &str) -> eyre::Result<GitWorktreeWrapper> {
833 let temp_dir = tempfile::tempdir()?;
834 let worktree_path = temp_dir.path().join(worktree_name);
835 git.run(&[
836 "worktree",
837 "add",
838 "--detach",
839 worktree_path.to_str().unwrap(),
840 ])?;
841 let worktree = Git {
842 repo_path: worktree_path,
843 ..git.clone()
844 };
845 Ok(GitWorktreeWrapper { temp_dir, worktree })
846}
847
848pub fn extract_hint_command(stdout: &str) -> Vec<String> {
851 let hint_command = stdout
852 .split_once("disable this hint by running: ")
853 .map(|(_first, second)| second)
854 .unwrap()
855 .split('\n')
856 .next()
857 .unwrap();
858 hint_command
859 .split(' ')
860 .skip(1) .filter(|s| s != &"--global")
862 .map(|s| s.to_owned())
863 .collect_vec()
864}
865
866pub fn remove_rebase_lines(output: String) -> String {
869 output
870 .lines()
871 .filter(|line| !line.contains("First, rewinding head") && !line.contains("Applying:"))
872 .filter(|line| {
873 !line.contains("Auto-merging")
882 && !line.contains("use \"git pull\"")
886 })
887 .flat_map(|line| [line, "\n"])
888 .collect()
889}
890
891pub fn trim_lines(output: String) -> String {
893 output
894 .lines()
895 .flat_map(|line| vec![line.trim_end(), "\n"].into_iter())
896 .collect()
897}
898
899pub fn remove_nondeterministic_lines(output: String) -> String {
901 output
902 .lines()
903 .filter(|line| {
904 !line.contains("Fetching")
906 && !line.contains("Your branch is up to date")
908 && !line.contains("Switched to branch")
912 && !line.contains("hint:")
914 && !line.is_empty()
919 })
920 .flat_map(|line| [line, "\n"])
921 .collect()
922}
923
924pub mod pty {
926 use std::sync::{Arc, Mutex, mpsc::channel};
927 use std::thread;
928 use std::time::Duration;
929
930 use eyre::eyre;
931 use portable_pty::{CommandBuilder, ExitStatus, PtySize, native_pty_system};
932
933 use super::Git;
934
935 pub const UP_ARROW: &str = "\x1b[A";
937
938 pub const DOWN_ARROW: &str = "\x1b[B";
940
941 pub enum PtyAction<'a> {
943 Write(&'a str),
945
946 WaitUntilContains(&'a str),
949 }
950
951 #[track_caller]
953 pub fn run_in_pty(
954 git: &Git,
955 branchless_subcommand: &str,
956 args: &[&str],
957 inputs: &[PtyAction],
958 ) -> eyre::Result<ExitStatus> {
959 let pty_system = native_pty_system();
961 let pty_size = PtySize::default();
962 let pty = pty_system
963 .openpty(pty_size)
964 .map_err(|e| eyre!("Could not open pty: {}", e))?;
965 let mut pty_master = pty
966 .master
967 .take_writer()
968 .map_err(|e| eyre!("Could not take PTY master writer: {e}"))?;
969
970 let mut cmd = CommandBuilder::new(&git.path_to_git);
972 cmd.env_clear();
973 for (k, v) in git.get_base_env(0) {
974 cmd.env(k, v);
975 }
976 cmd.env("TERM", "xterm");
977 cmd.arg("branchless");
978 cmd.arg(branchless_subcommand);
979 cmd.args(args);
980 cmd.cwd(&git.repo_path);
981
982 let mut child = pty
983 .slave
984 .spawn_command(cmd)
985 .map_err(|e| eyre!("Could not spawn child: {}", e))?;
986
987 let reader = pty
988 .master
989 .try_clone_reader()
990 .map_err(|e| eyre!("Could not clone reader: {}", e))?;
991 let reader = Arc::new(Mutex::new(reader));
992
993 let parser = vt100::Parser::new(pty_size.rows, pty_size.cols, 0);
994 let parser = Arc::new(Mutex::new(parser));
995
996 for action in inputs {
997 match action {
998 PtyAction::WaitUntilContains(value) => {
999 let (finished_tx, finished_rx) = channel();
1000
1001 let wait_thread = {
1002 let parser = Arc::clone(&parser);
1003 let reader = Arc::clone(&reader);
1004 let value = value.to_string();
1005 thread::spawn(move || -> anyhow::Result<()> {
1006 loop {
1007 {
1011 let parser = parser.lock().unwrap();
1012 if parser.screen().contents().contains(&value) {
1013 break;
1014 }
1015 }
1016
1017 let mut reader = reader.lock().unwrap();
1018 const BUF_SIZE: usize = 4096;
1019 let mut buffer = [0; BUF_SIZE];
1020 let n = reader.read(&mut buffer)?;
1021 assert!(n < BUF_SIZE, "filled up PTY buffer by reading {n} bytes",);
1022
1023 {
1024 let mut parser = parser.lock().unwrap();
1025 parser.process(&buffer[..n]);
1026 }
1027 }
1028
1029 finished_tx.send(()).unwrap();
1030 Ok(())
1031 })
1032 };
1033
1034 if finished_rx.recv_timeout(Duration::from_secs(5)).is_err() {
1035 panic!(
1036 "\
1037Timed out waiting for virtual terminal to show string: {:?}
1038Screen contents:
1039-----
1040{}
1041-----
1042",
1043 value,
1044 parser.lock().unwrap().screen().contents(),
1045 );
1046 }
1047
1048 wait_thread.join().unwrap().unwrap();
1049 }
1050
1051 PtyAction::Write(value) => {
1052 if let Ok(Some(exit_status)) = child.try_wait() {
1053 panic!(
1054 "\
1055Tried to write {value:?} to PTY, but the process has already exited with status {exit_status:?}. Screen contents:
1056-----
1057{}
1058-----
1059", parser.lock().unwrap().screen().contents(),
1060 );
1061 }
1062 write!(pty_master, "{value}")?;
1063 pty_master.flush()?;
1064 }
1065 }
1066 }
1067
1068 let read_remainder_of_pty_output_thread = thread::spawn({
1069 let reader = Arc::clone(&reader);
1070 move || {
1071 let mut reader = reader.lock().unwrap();
1072 let mut buffer = Vec::new();
1073 reader.read_to_end(&mut buffer).expect("finish reading pty");
1074 String::from_utf8(buffer).unwrap()
1075 }
1076 });
1077 let exit_status = child.wait()?;
1078
1079 let _ = read_remainder_of_pty_output_thread;
1080 Ok(exit_status)
1089 }
1090}