| 48 | 48 | | fn native(name: &str, docker_hosted: bool) -> bool { |
| 49 | 49 | | matches!( |
| 50 | 50 | | name, |
| 51 | | − | "actions/checkout" |
| 52 | | − | | "actions/upload-artifact" |
| 51 | + | "actions/upload-artifact" |
| 53 | 52 | | | "actions/upload-artifact/merge" |
| 54 | 53 | | | "actions/download-artifact" |
| 55 | 54 | | ) || (docker_hosted && name == "docker/setup-buildx-action") |
| ⋯ |
| 70 | 69 | | let mut parts = name.splitn(3, '/'); |
| 71 | 70 | | let (owner, repo) = (parts.next()?, parts.next()?); |
| 72 | 71 | | (!owner.is_empty() && !repo.is_empty()).then(|| (owner.to_owned(), repo.to_owned(), git_ref.to_owned())) |
| 73 | | − | } |
| 74 | | − | |
| 75 | | − | /// Every branch, as a checkout with all history fetches them. |
| 76 | | − | const HEADS_REFSPEC: &str = "+refs/heads/*:refs/remotes/origin/*"; |
| 77 | | − | /// Every tag, likewise. |
| 78 | | − | const TAGS_REFSPEC: &str = "+refs/tags/*:refs/tags/*"; |
| 79 | | − | |
| 80 | | − | /// What a checkout fetches and where it ends, as `actions/checkout` |
| 81 | | − | /// decides it (its `getRefSpec`, `getRefSpecForAllHistory` and |
| 82 | | − | /// `getCheckoutInfo`). |
| 83 | | − | #[derive(Debug, PartialEq, Eq)] |
| 84 | | − | struct CheckoutPlan { |
| 85 | | − | refspecs: Vec<String>, |
| 86 | | − | /// The commit to end on, when it is known before the fetch. |
| 87 | | − | sha: Option<String>, |
| 88 | | − | /// The branch made there (or at its remote-tracking branch). |
| 89 | | − | branch: Option<String>, |
| 90 | | − | /// Where to end with neither: a tag, or another ref. |
| 91 | | − | start: Option<String>, |
| 92 | | − | /// A name that may be a branch or a tag; the fetch tells which. |
| 93 | | − | unqualified: Option<String>, |
| 94 | | − | /// The step's `ref` output. |
| 95 | | − | output_ref: String, |
| 96 | | − | } |
| 97 | | − | |
| 98 | | − | /// `wanted` is the step's `ref` (or another repository's default branch); |
| 99 | | − | /// without one, the run's own ref and commit for its own repository. |
| 100 | | − | fn checkout_plan(wanted: Option<&str>, same: bool, run_ref: &str, run_sha: &str) -> CheckoutPlan { |
| 101 | | − | let (git_ref, commit) = match wanted { |
| 102 | | − | Some(r) if is_sha(r) => (String::new(), r.to_owned()), |
| 103 | | − | Some(r) => (r.to_owned(), String::new()), |
| 104 | | − | // Some events name a branch without `refs/heads/`. |
| 105 | | − | None if same && !run_sha.is_empty() && !run_ref.is_empty() && !run_ref.starts_with("refs/") => (format!("refs/heads/{run_ref}"), run_sha.to_owned()), |
| 106 | | − | None if same => (run_ref.to_owned(), run_sha.to_owned()), |
| 107 | | − | None => (String::new(), String::new()), |
| 108 | | − | }; |
| 109 | | − | let upper = git_ref.to_ascii_uppercase(); |
| 110 | | − | let rest = |prefix: &str| git_ref[prefix.len()..].to_owned(); |
| 111 | | − | let mut plan = CheckoutPlan { refspecs: Vec::new(), sha: None, branch: None, start: None, unqualified: None, output_ref: git_ref.clone() }; |
| 112 | | − | if !commit.is_empty() { |
| 113 | | − | plan.refspecs.push(if upper.starts_with("REFS/HEADS/") { |
| 114 | | − | plan.branch = Some(rest("refs/heads/")); |
| 115 | | − | format!("+{commit}:refs/remotes/origin/{}", rest("refs/heads/")) |
| 116 | | − | } else if upper.starts_with("REFS/PULL/") { |
| 117 | | − | format!("+{commit}:refs/remotes/pull/{}", rest("refs/pull/")) |
| 118 | | − | } else if upper.starts_with("REFS/TAGS/") { |
| 119 | | − | format!("+{commit}:{git_ref}") |
| 120 | | − | } else { |
| 121 | | − | commit.clone() |
| 122 | | − | }); |
| 123 | | − | plan.sha = Some(commit); |
| 124 | | − | } else if git_ref.is_empty() { |
| 125 | | − | plan.refspecs.push("HEAD".into()); |
| 126 | | − | } else if !upper.starts_with("REFS/") { |
| 127 | | − | plan.refspecs.push(format!("+refs/heads/{git_ref}*:refs/remotes/origin/{git_ref}*")); |
| 128 | | − | plan.refspecs.push(format!("+refs/tags/{git_ref}*:refs/tags/{git_ref}*")); |
| 129 | | − | plan.unqualified = Some(git_ref.clone()); |
| 130 | | − | } else if upper.starts_with("REFS/HEADS/") { |
| 131 | | − | plan.branch = Some(rest("refs/heads/")); |
| 132 | | − | plan.refspecs.push(format!("+{git_ref}:refs/remotes/origin/{}", rest("refs/heads/"))); |
| 133 | | − | } else if upper.starts_with("REFS/PULL/") { |
| 134 | | − | let local = format!("refs/remotes/pull/{}", rest("refs/pull/")); |
| 135 | | − | plan.refspecs.push(format!("+{git_ref}:{local}")); |
| 136 | | − | plan.start = Some(local); |
| 137 | | − | } else { |
| 138 | | − | plan.refspecs.push(format!("+{git_ref}:{git_ref}")); |
| 139 | | − | plan.start = Some(git_ref.clone()); |
| 140 | | − | } |
| 141 | | − | plan |
| 142 | 72 | | } |
| 143 | 73 | | |
| 144 | 74 | | impl Job { |
| ⋯ |
| 170 | 100 | | } |
| 171 | 101 | | } |
| 172 | 102 | | false |
| 173 | | − | } |
| 174 | | − | |
| 175 | | − | /// A git command with `header` sent only to `server` (`http.<server>/.extraheader`), |
| 176 | | − | /// so a submodule on another host never sees the job's token. Logged |
| 177 | | − | /// without the header. |
| 178 | | − | fn git_scoped(&mut self, dir: &Path, args: &[&str], server: &str, header: Option<&str>) -> bool { |
| 179 | | − | self.log.line(&format!("[command]git {}", args.join(" "))); |
| 180 | | − | let mut command = Command::new("git"); |
| 181 | | − | command.current_dir(dir); |
| 182 | | − | if let Some(header) = header { |
| 183 | | − | command.args(["-c", &format!("http.{server}/.extraheader={header}")]); |
| 184 | | − | } |
| 185 | | − | command.args(args).env("GIT_TERMINAL_PROMPT", "0"); |
| 186 | | − | let mut commands = Commands::default(); |
| 187 | | − | matches!(process::run(command, Duration::from_secs(600).min(self.remaining_time()), &mut self.log, &mut commands), Ok(Ended::Exited(0))) |
| 188 | 103 | | } |
| 189 | 104 | | |
| 190 | | − | /// `actions/checkout`, against g1t: the inputs of its version 4 and 5 |
| 191 | | − | /// but `ssh-key`, `lfs` and `clean: false`, fetching what it fetches |
| 192 | | − | /// and ending on the same branch or commit. |
| 193 | | − | fn checkout(&mut self, with: &BTreeMap<String, String>) -> (bool, BTreeMap<String, String>) { |
| 194 | | − | let checkout = self.spec["checkout"].clone(); |
| 195 | | − | let own = checkout["repository"].as_str().unwrap_or_default().to_owned(); |
| 196 | | − | let server = self.contexts["github"]["server_url"].as_str().unwrap_or("https://g1t.sh").trim_end_matches('/').to_owned(); |
| 197 | | − | let input = |name: &str| with.get(name).map(|v| v.trim().to_owned()).filter(|v| !v.is_empty()); |
| 198 | | − | let yes = |name: &str, default: bool| input(name).map_or(default, |v| v.eq_ignore_ascii_case("true")); |
| 199 | | − | let repository = input("repository").unwrap_or(own.clone()); |
| 200 | | − | let same = repository.eq_ignore_ascii_case(&own); |
| 201 | | − | let token = input("token").or_else(|| checkout["token"].as_str().map(str::to_owned)).unwrap_or_default(); |
| 202 | | − | let url = if same { checkout["url"].as_str().unwrap_or_default().to_owned() } else { format!("{server}/{repository}.git") }; |
| 203 | | − | let path = input("path").map_or(self.workspace.clone(), |p| self.workspace.join(p)); |
| 204 | | − | // As the action reads it: not a number, or below zero, is all history. |
| 205 | | − | let depth: u32 = input("fetch-depth").map_or(1, |d| d.parse::<f64>().ok().filter(|d| *d >= 0.0).map_or(0, |d| d as u32)); |
| 206 | | − | let wanted_ref = input("ref"); |
| 207 | | − | let filter = input("filter"); |
| 208 | | − | let sparse: Vec<String> = with.get("sparse-checkout").map(|s| s.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_owned).collect()).unwrap_or_default(); |
| 209 | | − | let auth = (!token.is_empty()).then(|| format!("AUTHORIZATION: basic {}", STANDARD.encode(format!("x-access-token:{token}")))); |
| 210 | | − | let header = auth.as_deref(); |
| 211 | | − | |
| 212 | | − | self.log.line(&format!("Syncing repository: {repository}")); |
| 213 | | − | if path.exists() { |
| 214 | | − | let _ = std::fs::remove_dir_all(&path); |
| 215 | | − | } |
| 216 | | − | if let Err(error) = std::fs::create_dir_all(&path) { |
| 217 | | − | self.log.line(&format!("##[error]Could not make {}: {error}", path.display())); |
| 218 | | − | return (false, BTreeMap::new()); |
| 219 | | − | } |
| 220 | | − | // So git works on it in a container run as another user. |
| 221 | | − | if yes("set-safe-directory", true) { |
| 222 | | − | let _ = Command::new("git").args(["config", "--global", "--add", "safe.directory", "*"]).status(); |
| 223 | | − | } |
| 224 | | − | if !self.git(&path, &["init", "--quiet"], None) || !self.git(&path, &["remote", "add", "origin", &url], None) { |
| 225 | | − | return (false, BTreeMap::new()); |
| 226 | | − | } |
| 227 | | − | let _ = Command::new("git").current_dir(&path).args(["config", "--local", "gc.auto", "0"]).status(); |
| 228 | | − | |
| 229 | | − | // Another repository with no ref: its default branch, as the |
| 230 | | − | // action asks the API for it. |
| 231 | | − | let default_branch = match (&wanted_ref, same) { |
| 232 | | − | (None, false) => self.default_branch(&path, &server, header), |
| 233 | | − | _ => None, |
| 234 | | − | }; |
| 235 | | − | let plan = checkout_plan( |
| 236 | | − | wanted_ref.as_deref().or(default_branch.as_deref()), |
| 237 | | − | same, |
| 238 | | − | checkout["ref"].as_str().unwrap_or_default(), |
| 239 | | − | checkout["sha"].as_str().unwrap_or_default(), |
| 240 | | − | ); |
| 241 | | − | let depth_arg = format!("--depth={depth}"); |
| 242 | | − | let filter_arg = filter.clone().or_else(|| (!sparse.is_empty()).then(|| "blob:none".to_owned())).map(|f| format!("--filter={f}")); |
| 243 | | − | let fetch_args = |refspecs: &[String], tags: bool| { |
| 244 | | − | let mut args: Vec<String> = ["-c", "protocol.version=2", "fetch", "--prune", "--no-recurse-submodules", "--quiet"].map(String::from).into(); |
| 245 | | − | if !tags { |
| 246 | | − | args.push("--no-tags".into()); |
| 247 | | − | } |
| 248 | | − | args.extend(filter_arg.clone()); |
| 249 | | − | if depth > 0 { |
| 250 | | − | args.push(depth_arg.clone()); |
| 251 | | − | } |
| 252 | | − | args.push("origin".into()); |
| 253 | | − | args.extend(refspecs.iter().cloned()); |
| 254 | | − | args |
| 255 | | − | }; |
| 256 | | − | // All history: every branch and tag, then what the run names, which |
| 257 | | − | // may be a commit its branch has since moved on from. |
| 258 | | − | let everything = fetch_args(&[HEADS_REFSPEC.into(), TAGS_REFSPEC.into()], true); |
| 259 | | − | let everything: Vec<&str> = everything.iter().map(String::as_str).collect(); |
| 260 | | − | let named = fetch_args(&plan.refspecs, yes("fetch-tags", false)); |
| 261 | | − | let named: Vec<&str> = named.iter().map(String::as_str).collect(); |
| 262 | | − | let fetched = (depth > 0 || self.fetch_retrying(&path, &everything, header)) && self.fetch_retrying(&path, &named, header); |
| 263 | | − | let has = |target: &str| Command::new("git").current_dir(&path).args(["cat-file", "-e", &format!("{target}^{{commit}}")]).status().is_ok_and(|s| s.success()); |
| 264 | | − | // The commit may be further back than a shallow fetch reaches (the |
| 265 | | − | // branch moved on after the run began), or not one the server |
| 266 | | − | // gives by itself: then the whole history. |
| 267 | | − | if let Some(sha) = plan.sha.as_deref().filter(|sha| !has(sha)) { |
| 268 | | − | let shallow = path.join(".git").join("shallow").exists(); |
| 269 | | − | let mut deepen = vec!["-c", "protocol.version=2", "fetch", "--no-tags", "--quiet"]; |
| 270 | | − | if shallow { |
| 271 | | − | deepen.push("--unshallow"); |
| 272 | | − | } |
| 273 | | − | deepen.extend(["origin", HEADS_REFSPEC]); |
| 274 | | − | if !self.fetch_retrying(&path, &deepen, header) || !has(sha) { |
| 275 | | − | self.log.line(&format!("##[error]Could not fetch {sha} from {repository}.")); |
| 276 | | − | return (false, BTreeMap::new()); |
| 277 | | − | } |
| 278 | | − | } else if !fetched { |
| 279 | | − | self.log.line(&format!("##[error]Could not fetch {} from {repository}.", plan.refspecs.join(" "))); |
| 280 | | − | return (false, BTreeMap::new()); |
| 281 | | − | } |
| 282 | | − | |
| 283 | | − | // Where to end up: a branch made at its start point, or detached. |
| 284 | | − | let ref_exists = |name: &str| Command::new("git").current_dir(&path).args(["rev-parse", "--verify", "--quiet", name]).output().is_ok_and(|o| o.status.success()); |
| 285 | | − | let (branch, target) = match (&plan.sha, &plan.branch, &plan.unqualified) { |
| 286 | | − | (Some(sha), branch, _) => (branch.clone(), sha.clone()), |
| 287 | | − | (None, Some(branch), _) => (Some(branch.clone()), format!("refs/remotes/origin/{branch}")), |
| 288 | | − | (None, None, Some(name)) if ref_exists(&format!("refs/remotes/origin/{name}")) => (Some(name.clone()), format!("refs/remotes/origin/{name}")), |
| 289 | | − | (None, None, Some(name)) if ref_exists(&format!("refs/tags/{name}")) => (None, format!("refs/tags/{name}")), |
| 290 | | − | (None, None, Some(name)) => { |
| 291 | | − | self.log.line(&format!("##[error]A branch or tag with the name '{name}' could not be found")); |
| 292 | | − | return (false, BTreeMap::new()); |
| 293 | | − | } |
| 294 | | − | (None, None, None) => (None, plan.start.clone().unwrap_or_else(|| "FETCH_HEAD".into())), |
| 295 | | − | }; |
| 296 | | − | if !sparse.is_empty() { |
| 297 | | − | let mut args = vec!["sparse-checkout", "set"]; |
| 298 | | − | args.push(if yes("sparse-checkout-cone-mode", true) { "--cone" } else { "--no-cone" }); |
| 299 | | − | args.extend(sparse.iter().map(String::as_str)); |
| 300 | | − | if !self.git(&path, &args, None) { |
| 301 | | − | return (false, BTreeMap::new()); |
| 302 | | − | } |
| 303 | | − | } |
| 304 | | − | let checked_out = match &branch { |
| 305 | | − | Some(branch) => self.git(&path, &["checkout", "--quiet", "--force", "-B", branch, &target], None), |
| 306 | | − | None => self.git(&path, &["checkout", "--quiet", "--force", "--detach", &target], None), |
| 307 | | − | }; |
| 308 | | − | if !checked_out { |
| 309 | | − | return (false, BTreeMap::new()); |
| 310 | | − | } |
| 311 | | − | let persist = yes("persist-credentials", true); |
| 312 | | − | let key = format!("http.{server}/.extraheader"); |
| 313 | | − | if persist && let Some(header) = header { |
| 314 | | − | let _ = Command::new("git").current_dir(&path).args(["config", "--local", &key, header]).status(); |
| 315 | | − | } |
| 316 | | − | if let Some(submodules) = input("submodules").map(|s| s.to_ascii_lowercase()).filter(|s| s == "true" || s == "recursive") { |
| 317 | | − | let recursive = submodules == "recursive"; |
| 318 | | − | // A submodule on g1t named by its SSH address is fetched over |
| 319 | | − | // HTTPS with the token, as the action does. |
| 320 | | − | let host = server.split_once("://").map_or(server.as_str(), |(_, host)| host).to_owned(); |
| 321 | | − | let instead = format!("url.{server}/.insteadOf=git@{host}:"); |
| 322 | | − | let mut sync = vec!["submodule", "sync", "--quiet"]; |
| 323 | | − | let mut update = vec!["-c", &instead, "-c", "protocol.version=2", "submodule", "update", "--init", "--force", "--quiet"]; |
| 324 | | − | if depth > 0 { |
| 325 | | − | update.push(&depth_arg); |
| 326 | | − | } |
| 327 | | − | if recursive { |
| 328 | | − | sync.push("--recursive"); |
| 329 | | − | update.push("--recursive"); |
| 330 | | − | } |
| 331 | | − | if !self.git(&path, &sync, None) || !self.git_scoped(&path, &update, &server, header) { |
| 332 | | − | self.log.line("##[error]The submodules could not all be checked out."); |
| 333 | | − | return (false, BTreeMap::new()); |
| 334 | | − | } |
| 335 | | − | if persist && let Some(header) = header { |
| 336 | | − | let mut foreach = vec!["submodule", "foreach", "--quiet"]; |
| 337 | | − | if recursive { |
| 338 | | − | foreach.push("--recursive"); |
| 339 | | − | } |
| 340 | | − | foreach.push(r#"git config --local "$G1T_KEY" "$G1T_HEADER" && git config --local gc.auto 0"#); |
| 341 | | − | let _ = Command::new("git").current_dir(&path).args(&foreach).env("G1T_KEY", &key).env("G1T_HEADER", header).status(); |
| 342 | | − | } |
| 343 | | − | } |
| 344 | | − | if yes("lfs", false) { |
| 345 | | − | self.log.line("##[warning]Git LFS files are not fetched on g1t: g1t does not keep LFS objects yet."); |
| 346 | | − | } |
| 347 | | − | let commit = Command::new("git").current_dir(&path).args(["rev-parse", "HEAD"]).output().ok().map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned()).unwrap_or_default(); |
| 348 | | − | self.log.line(&format!("Checked out {commit}")); |
| 349 | | − | let mut outputs = BTreeMap::new(); |
| 350 | | − | outputs.insert("ref".into(), plan.output_ref); |
| 351 | | − | outputs.insert("commit".into(), commit); |
| 352 | | − | (true, outputs) |
| 353 | | − | } |
| 354 | | − | |
| 355 | | − | /// Another repository's default branch, from what its remote says |
| 356 | | − | /// `HEAD` is. None when it says nothing: the fetch then takes `HEAD`. |
| 357 | | − | fn default_branch(&mut self, dir: &Path, server: &str, header: Option<&str>) -> Option<String> { |
| 358 | | − | let mut command = Command::new("git"); |
| 359 | | − | command.current_dir(dir); |
| 360 | | − | if let Some(header) = header { |
| 361 | | − | command.args(["-c", &format!("http.{server}/.extraheader={header}")]); |
| 362 | | − | } |
| 363 | | − | let output = command.args(["ls-remote", "--quiet", "--symref", "origin", "HEAD"]).env("GIT_TERMINAL_PROMPT", "0").output().ok()?; |
| 364 | | − | let text = String::from_utf8_lossy(&output.stdout); |
| 365 | | − | let branch = text.lines().find_map(|line| line.strip_prefix("ref: ")?.split_once('\t')?.0.strip_prefix("refs/heads/").map(str::to_owned))?; |
| 366 | | − | self.log.line(&format!("Default branch '{branch}'")); |
| 367 | | − | Some(format!("refs/heads/{branch}")) |
| 368 | | − | } |
| 369 | | − | |
| 370 | 105 | | /// Fetches another repository's action, once per job: from g1t when |
| 371 | 106 | | /// g1t has the repository and this one may use it, otherwise from |
| 372 | 107 | | /// GitHub. A private repository on g1t that may not be used here fails |
| ⋯ |
| 649 | 384 | | return self.setup_buildx(with); |
| 650 | 385 | | } |
| 651 | 386 | | match lower.as_str() { |
| 652 | | − | "actions/checkout" => return self.checkout(with), |
| 653 | 387 | | "actions/upload-artifact" => return self.upload_artifact(with), |
| 654 | 388 | | "actions/upload-artifact/merge" => return self.merge_artifacts(with), |
| 655 | 389 | | "actions/download-artifact" => return self.download_artifact(with), |
| ⋯ |
| 1051 | 785 | | let found = |uses: &str| remote_action(uses, true); |
| 1052 | 786 | | assert_eq!(found("actions/setup-node@v4"), Some(("actions".into(), "setup-node".into(), "v4".into()))); |
| 1053 | 787 | | assert_eq!(found(" github/codeql-action/init@v3 "), Some(("github".into(), "codeql-action".into(), "v3".into()))); |
| 1054 | | − | // Done by the runner itself, local, an image, or no version. |
| 1055 | | − | assert_eq!(found("actions/checkout@v4"), None); |
| 1056 | | − | // actions/cache is the real action, fetched like any other. |
| 788 | + | // actions/checkout and actions/cache are the real actions, fetched |
| 789 | + | // like any other. |
| 790 | + | assert_eq!(found("actions/checkout@v4"), Some(("actions".into(), "checkout".into(), "v4".into()))); |
| 1057 | 791 | | assert_eq!(found("Actions/Cache/Restore@v4"), Some(("Actions".into(), "Cache".into(), "v4".into()))); |
| 792 | + | // Done by the runner itself, local, an image, or no version. |
| 1058 | 793 | | assert_eq!(found("docker/setup-buildx-action@v3"), None); |
| 1059 | 794 | | assert!(remote_action("docker/setup-buildx-action@v3", false).is_some()); |
| 1060 | 795 | | assert_eq!(found("./.g1t/actions/setup"), None); |
| 1061 | 796 | | assert_eq!(found("docker://alpine:3"), None); |
| 1062 | 797 | | assert_eq!(found("actions/setup-node"), None); |
| 1063 | 798 | | assert_eq!(found("setup-node@v4"), None); |
| 1064 | | − | } |
| 1065 | | − | |
| 1066 | | − | #[test] |
| 1067 | | − | fn a_checkout_fetches_what_actions_checkout_fetches() { |
| 1068 | | − | let sha = "0123456789abcdef0123456789abcdef01234567"; |
| 1069 | | − | let specs = |plan: &CheckoutPlan| plan.refspecs.clone(); |
| 1070 | | − | // A push: the run's commit, as its branch. |
| 1071 | | − | let push = checkout_plan(None, true, "refs/heads/main", sha); |
| 1072 | | − | assert_eq!(specs(&push), [format!("+{sha}:refs/remotes/origin/main")]); |
| 1073 | | − | assert_eq!((push.sha.as_deref(), push.branch.as_deref(), push.output_ref.as_str()), (Some(sha), Some("main"), "refs/heads/main")); |
| 1074 | | − | // An unqualified branch from the event is qualified first. |
| 1075 | | − | assert_eq!(checkout_plan(None, true, "main", sha).output_ref, "refs/heads/main"); |
| 1076 | | − | // A pull request: its commit under refs/remotes/pull, detached. |
| 1077 | | − | let pull = checkout_plan(None, true, "refs/pull/7/merge", sha); |
| 1078 | | − | assert_eq!(specs(&pull), [format!("+{sha}:refs/remotes/pull/7/merge")]); |
| 1079 | | − | assert_eq!(pull.branch, None); |
| 1080 | | − | // A tag push, and a deployment of a bare commit. |
| 1081 | | − | assert_eq!(specs(&checkout_plan(None, true, "refs/tags/v1", sha)), [format!("+{sha}:refs/tags/v1")]); |
| 1082 | | − | assert_eq!(specs(&checkout_plan(None, true, "", sha)), [sha]); |
| 1083 | | − | // `ref:` a commit, a branch, a tag or a name that may be either. |
| 1084 | | − | let by_sha = checkout_plan(Some(sha), true, "refs/heads/main", "ffff"); |
| 1085 | | − | assert_eq!((specs(&by_sha), by_sha.sha.as_deref(), by_sha.output_ref.as_str()), (vec![sha.to_owned()], Some(sha), "")); |
| 1086 | | − | let branch = checkout_plan(Some("refs/heads/dev"), true, "refs/heads/main", sha); |
| 1087 | | − | assert_eq!((specs(&branch), branch.sha, branch.branch.as_deref()), (vec!["+refs/heads/dev:refs/remotes/origin/dev".to_owned()], None, Some("dev"))); |
| 1088 | | − | let tag = checkout_plan(Some("refs/tags/v2"), true, "", ""); |
| 1089 | | − | assert_eq!((specs(&tag), tag.start.as_deref()), (vec!["+refs/tags/v2:refs/tags/v2".to_owned()], Some("refs/tags/v2"))); |
| 1090 | | − | let name = checkout_plan(Some("v2"), false, "", ""); |
| 1091 | | − | assert_eq!(specs(&name), ["+refs/heads/v2*:refs/remotes/origin/v2*", "+refs/tags/v2*:refs/tags/v2*"]); |
| 1092 | | − | assert_eq!(name.unqualified.as_deref(), Some("v2")); |
| 1093 | | − | // Another repository whose default branch is not known: its HEAD. |
| 1094 | | − | assert_eq!(specs(&checkout_plan(None, false, "refs/heads/main", sha)), ["HEAD"]); |
| 1095 | | − | } |
| 1096 | | − | |
| 1097 | | − | /// Checks out repositories of its own over `file://`, where git is |
| 1098 | | − | /// installed: the run's commit behind its branch's tip, all history, |
| 1099 | | − | /// a tag and a branch by name, a sparse checkout, and another |
| 1100 | | − | /// repository's default branch. |
| 1101 | | − | #[test] |
| 1102 | | − | fn a_checkout_ends_where_actions_checkout_ends() { |
| 1103 | | − | let git = |dir: &Path, args: &[&str]| { |
| 1104 | | − | let out = Command::new("git").current_dir(dir).args(args).output().expect("git"); |
| 1105 | | − | assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr)); |
| 1106 | | − | String::from_utf8_lossy(&out.stdout).trim().to_owned() |
| 1107 | | − | }; |
| 1108 | | − | if Command::new("git").arg("--version").output().is_err() { |
| 1109 | | − | return; |
| 1110 | | − | } |
| 1111 | | − | let root = std::env::temp_dir().join(format!("g1t-checkout-test-{}", std::process::id())); |
| 1112 | | − | let _ = std::fs::remove_dir_all(&root); |
| 1113 | | − | let make = |name: &str, branch: &str| { |
| 1114 | | − | let dir = root.join("src").join(name); |
| 1115 | | − | std::fs::create_dir_all(&dir).unwrap(); |
| 1116 | | − | git(&dir, &["init", "--quiet", "-b", branch]); |
| 1117 | | − | for (key, value) in [("user.name", "t"), ("user.email", "t@example.com"), ("commit.gpgsign", "false"), ("tag.gpgsign", "false")] { |
| 1118 | | − | git(&dir, &["config", key, value]); |
| 1119 | | − | } |
| 1120 | | − | dir |
| 1121 | | − | }; |
| 1122 | | − | let commit = |dir: &Path, file: &str, message: &str| { |
| 1123 | | − | std::fs::create_dir_all(dir.join(file).parent().unwrap()).unwrap(); |
| 1124 | | − | std::fs::write(dir.join(file), message).unwrap(); |
| 1125 | | − | git(dir, &["add", "."]); |
| 1126 | | − | git(dir, &["commit", "--quiet", "-m", message]); |
| 1127 | | − | }; |
| 1128 | | − | let app = make("app", "main"); |
| 1129 | | − | commit(&app, "docs/a.md", "one"); |
| 1130 | | − | commit(&app, "src/main.rs", "two"); |
| 1131 | | − | git(&app, &["tag", "v1"]); |
| 1132 | | − | git(&app, &["checkout", "--quiet", "-b", "feature"]); |
| 1133 | | − | commit(&app, "feature.txt", "feature"); |
| 1134 | | − | git(&app, &["checkout", "--quiet", "main"]); |
| 1135 | | − | commit(&app, "README.md", "three"); |
| 1136 | | − | let other = make("other", "trunk"); |
| 1137 | | − | commit(&other, "o.txt", "o"); |
| 1138 | | − | let served = root.join("srv").join("acme"); |
| 1139 | | − | std::fs::create_dir_all(&served).unwrap(); |
| 1140 | | − | for name in ["app", "other"] { |
| 1141 | | − | let src = root.join("src").join(name); |
| 1142 | | − | git(&served, &["clone", "--quiet", "--bare", &src.display().to_string(), &format!("{name}.git")]); |
| 1143 | | − | git(&served.join(format!("{name}.git")), &["config", "uploadpack.allowFilter", "true"]); |
| 1144 | | − | } |
| 1145 | | − | let server = format!("file://{}", root.join("srv").display()); |
| 1146 | | − | let behind = git(&app, &["rev-parse", "main~1"]); |
| 1147 | | − | let spec = json!({ |
| 1148 | | − | "checkout": { "repository": "acme/app", "url": format!("{server}/acme/app.git"), "ref": "refs/heads/main", "sha": behind, "token": "" }, |
| 1149 | | − | "github": { "server_url": server }, |
| 1150 | | − | "variables": {}, |
| 1151 | | − | "contexts": {}, |
| 1152 | | − | }); |
| 1153 | | − | let workspace = root.join("work"); |
| 1154 | | − | let mut job = super::super::setup_in(spec, super::super::report::Api::offline(), workspace.clone(), root.join("temp")).unwrap(); |
| 1155 | | − | let mut run = |inputs: &[(&str, &str)]| { |
| 1156 | | − | let mut with: BTreeMap<String, String> = inputs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); |
| 1157 | | − | with.insert("set-safe-directory".into(), "false".into()); |
| 1158 | | − | job.checkout(&with) |
| 1159 | | − | }; |
| 1160 | | − | |
| 1161 | | − | // A push whose branch moved on: the run's commit, on its branch. |
| 1162 | | − | let (ok, outputs) = run(&[]); |
| 1163 | | − | assert!(ok); |
| 1164 | | − | assert_eq!((git(&workspace, &["rev-parse", "HEAD"]), git(&workspace, &["branch", "--show-current"])), (behind.clone(), "main".to_owned())); |
| 1165 | | − | assert_eq!((outputs["ref"].as_str(), outputs["commit"].as_str()), ("refs/heads/main", behind.as_str())); |
| 1166 | | − | // All history: every branch and tag. |
| 1167 | | − | assert!(run(&[("fetch-depth", "0")]).0); |
| 1168 | | − | assert!(!workspace.join(".git/shallow").exists()); |
| 1169 | | − | assert_eq!(git(&workspace, &["branch", "-r"]).split_whitespace().collect::<Vec<_>>(), ["origin/feature", "origin/main"]); |
| 1170 | | − | assert_eq!(git(&workspace, &["tag"]), "v1"); |
| 1171 | | − | // A tag by name, detached; a branch by name, as the branch. |
| 1172 | | − | assert!(run(&[("ref", "v1")]).0); |
| 1173 | | − | assert_eq!((git(&workspace, &["describe", "--tags"]), git(&workspace, &["branch", "--show-current"])), ("v1".to_owned(), String::new())); |
| 1174 | | − | assert!(run(&[("ref", "feature")]).0); |
| 1175 | | − | assert_eq!(git(&workspace, &["branch", "--show-current"]), "feature"); |
| 1176 | | − | // Sparse: only the folders named, from a blobless fetch. |
| 1177 | | − | assert!(run(&[("sparse-checkout", "docs\n")]).0); |
| 1178 | | − | assert!(workspace.join("docs/a.md").exists() && !workspace.join("src").exists()); |
| 1179 | | − | assert_eq!(git(&workspace, &["config", "remote.origin.partialclonefilter"]), "blob:none"); |
| 1180 | | − | // Another repository without a ref: its default branch. |
| 1181 | | − | let (ok, outputs) = run(&[("repository", "acme/other"), ("path", "other")]); |
| 1182 | | − | assert!(ok); |
| 1183 | | − | assert_eq!(git(&workspace.join("other"), &["branch", "--show-current"]), "trunk"); |
| 1184 | | − | assert_eq!(outputs["ref"], "refs/heads/trunk"); |
| 1185 | | − | let _ = std::fs::remove_dir_all(&root); |
| 1186 | 799 | | } |
| 1187 | 800 | | |
| 1188 | 801 | | #[test] |