1use std::path::{Path, PathBuf};
17
18use strop_core::worker::CancelToken;
19use strop_workspace::ContainerId;
20
21use crate::exec::GitExec;
22use crate::remote::{context_with, discover_with, RemoteGitError};
23use crate::target::RepoTarget;
24use crate::GitContext;
25
26pub fn discover(
32 id: &ContainerId,
33 from: &Path,
34 cancel: &CancelToken,
35) -> Result<Option<PathBuf>, RemoteGitError> {
36 let exec = GitExec::Container {
37 container: id.clone(),
38 workdir: from,
39 };
40 discover_with(&exec, cancel)
41}
42
43pub fn context(
50 id: &ContainerId,
51 workdir: &Path,
52 cancel: &CancelToken,
53) -> Result<GitContext, RemoteGitError> {
54 let exec = GitExec::Container {
55 container: id.clone(),
56 workdir,
57 };
58 let repo = RepoTarget::Container {
59 container: id.clone(),
60 workdir: workdir.to_path_buf(),
61 };
62 context_with(&exec, repo, cancel)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::exec::GitRun;
69 use crate::remote::{context_from_runs, discover_from_run};
70
71 fn run(code: i32, stdout: &[u8], stderr: &[u8]) -> GitRun {
76 GitRun {
77 success: code == 0,
78 code: Some(code),
79 stdout: stdout.to_vec(),
80 stderr: stderr.to_vec(),
81 stdout_dropped: 0,
82 stderr_dropped: 0,
83 }
84 }
85
86 fn id() -> ContainerId {
87 ContainerId::canonical("a".repeat(64)).expect("64 hex is a canonical id")
88 }
89
90 #[test]
94 fn no_repo_is_none_and_other_failures_are_typed() {
95 let absent = run(
96 128,
97 b"",
98 b"fatal: not a git repository (or any of the parent directories): .git\n",
99 );
100 assert_eq!(discover_from_run(&absent), Ok(None));
101
102 let present = run(0, b"/work/app\n", b"");
103 assert_eq!(
104 discover_from_run(&present).unwrap(),
105 Some(PathBuf::from("/work/app"))
106 );
107
108 match discover_from_run(&run(128, b"", b"fatal: unsafe repository\n")) {
109 Err(RemoteGitError::Exit { op, code, stderr }) => {
110 assert_eq!(op, "rev-parse --show-toplevel");
111 assert_eq!(code, 128);
112 assert_eq!(stderr, "fatal: unsafe repository");
113 }
114 other => panic!("expected Exit, got {other:?}"),
115 }
116 }
117
118 #[test]
122 fn context_reports_container_target_and_head_state() {
123 let repo = RepoTarget::Container {
124 container: id(),
125 workdir: PathBuf::from("/work/app"),
126 };
127 let head = run(0, b"c59d8ceb7aeb96a1cdccff5646ec485acce32d45\n", b"");
128 let branch = run(128, b"", b"fatal: ref HEAD is not a symbolic ref\n");
129 let config = run(0, b"remote.origin.url\ngit@gh:acme/demo.git\0", b"");
130 let context = context_from_runs(repo.clone(), &head, &branch, &config).unwrap();
131 assert_eq!(context.repo, repo);
132 assert_eq!(
133 context.head_sha.as_deref(),
134 Some("c59d8ceb7aeb96a1cdccff5646ec485acce32d45")
135 );
136 assert_eq!(context.head_branch, None, "detached HEAD");
137 assert_eq!(
138 context.remotes,
139 vec![("origin".to_string(), "git@gh:acme/demo.git".to_string())]
140 );
141 }
142
143 #[test]
147 fn unborn_head_and_no_remotes_are_honest_data() {
148 let repo = RepoTarget::Container {
149 container: id(),
150 workdir: PathBuf::from("/work/app"),
151 };
152 let head = run(1, b"", b"");
153 let branch = run(0, b"main\n", b"");
154 let config = run(1, b"", b"");
155 let context = context_from_runs(repo, &head, &branch, &config).unwrap();
156 assert_eq!(context.head_sha, None, "unborn HEAD");
157 assert_eq!(context.head_branch.as_deref(), Some("main"));
158 assert!(context.remotes.is_empty());
159 }
160}