| 1 | //! Fixture helpers: build real repositories with the git CLI. |
| 2 | |
| 3 | #![allow(dead_code)] |
| 4 | |
| 5 | use std::path::{Path, PathBuf}; |
| 6 | use std::process::Command; |
| 7 | |
| 8 | /// Run git in `dir` with an isolated environment; panics on failure. |
| 9 | pub fn git(dir: &Path, args: &[&str]) -> String { |
| 10 | let home = std::env::temp_dir(); |
| 11 | let out = Command::new("git") |
| 12 | .args(args) |
| 13 | .current_dir(dir) |
| 14 | .env("GIT_CONFIG_NOSYSTEM", "1") |
| 15 | .env("GIT_CONFIG_GLOBAL", "/dev/null") |
| 16 | .env("HOME", home) |
| 17 | .env("GIT_AUTHOR_NAME", "Ada") |
| 18 | .env("GIT_AUTHOR_EMAIL", "ada@example.com") |
| 19 | .env("GIT_COMMITTER_NAME", "Ada") |
| 20 | .env("GIT_COMMITTER_EMAIL", "ada@example.com") |
| 21 | .env("GIT_TERMINAL_PROMPT", "0") |
| 22 | .output() |
| 23 | .expect("git runs"); |
| 24 | assert!( |
| 25 | out.status.success(), |
| 26 | "git {args:?} failed:\n{}", |
| 27 | String::from_utf8_lossy(&out.stderr) |
| 28 | ); |
| 29 | String::from_utf8_lossy(&out.stdout).trim().to_string() |
| 30 | } |
| 31 | |
| 32 | /// A bare repo at `root/rel` (created with `init --bare`). |
| 33 | pub fn bare(root: &Path, rel: &str, extra: &[&str]) -> PathBuf { |
| 34 | let path = root.join(rel); |
| 35 | std::fs::create_dir_all(&path).unwrap(); |
| 36 | let mut args = vec!["init", "--bare", "-q", "-b", "main"]; |
| 37 | args.extend_from_slice(extra); |
| 38 | git(&path, &args); |
| 39 | path |
| 40 | } |
| 41 | |
| 42 | /// A scratch clone of `bare` used to make commits and push them. |
| 43 | pub struct Work { |
| 44 | pub dir: tempfile::TempDir, |
| 45 | } |
| 46 | |
| 47 | impl Work { |
| 48 | pub fn clone_of(bare: &Path) -> Work { |
| 49 | let dir = tempfile::tempdir().unwrap(); |
| 50 | git(dir.path(), &["clone", "-q", bare.to_str().unwrap(), "."]); |
| 51 | git(dir.path(), &["checkout", "-q", "-B", "main"]); |
| 52 | Work { dir } |
| 53 | } |
| 54 | |
| 55 | pub fn path(&self) -> &Path { |
| 56 | self.dir.path() |
| 57 | } |
| 58 | |
| 59 | /// Write `file`, commit it and return the new commit id. |
| 60 | pub fn commit(&self, file: &str, content: &str, message: &str) -> String { |
| 61 | let path = self.path().join(file); |
| 62 | if let Some(parent) = path.parent() { |
| 63 | std::fs::create_dir_all(parent).unwrap(); |
| 64 | } |
| 65 | std::fs::write(&path, content).unwrap(); |
| 66 | git(self.path(), &["add", "-A"]); |
| 67 | git(self.path(), &["commit", "-q", "-m", message]); |
| 68 | git(self.path(), &["rev-parse", "HEAD"]) |
| 69 | } |
| 70 | |
| 71 | pub fn push(&self, refspec: &str) { |
| 72 | git(self.path(), &["push", "-q", "origin", refspec]); |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | /// Bare repo with one commit on `main` containing `files`. |
| 77 | pub fn repo_with(root: &Path, rel: &str, files: &[(&str, &str)]) -> (PathBuf, String) { |
| 78 | let bare = bare(root, rel, &[]); |
| 79 | let work = Work::clone_of(&bare); |
| 80 | let mut head = String::new(); |
| 81 | for (name, content) in files { |
| 82 | head = work.commit(name, content, &format!("add {name}")); |
| 83 | } |
| 84 | work.push("main"); |
| 85 | (bare, head) |
| 86 | } |
| 87 | |
| 88 | /// Ids of interesting objects in [`rich_repo`]. |
| 89 | pub struct Rich { |
| 90 | pub bare: PathBuf, |
| 91 | pub head: String, |
| 92 | pub first: String, |
| 93 | pub feature: String, |
| 94 | } |
| 95 | |
| 96 | /// A bare repo exercising the browsing edge cases: nested dirs, a README |
| 97 | /// with relative links, a binary and an oversize file, a unicode path, a |
| 98 | /// symlink, a submodule entry, a slashy branch and both kinds of tags. |
| 99 | pub fn rich_repo(root: &Path, rel: &str) -> Rich { |
| 100 | let bare = bare(root, rel, &[]); |
| 101 | let work = Work::clone_of(&bare); |
| 102 | let w = work.path(); |
| 103 | let write = |name: &str, data: &[u8]| { |
| 104 | let path = w.join(name); |
| 105 | std::fs::create_dir_all(path.parent().unwrap()).unwrap(); |
| 106 | std::fs::write(path, data).unwrap(); |
| 107 | }; |
| 108 | write( |
| 109 | "src/main.rs", |
| 110 | b"// entry\nfn main() {\n println!(\"<hi>\");\n}\n", |
| 111 | ); |
| 112 | let first = work.commit( |
| 113 | "README.md", |
| 114 | "# Rich\n\nSee [the guide](docs/guide.md) and .\n\n\ |
| 115 | <script>alert(1)</script>\n", |
| 116 | "first commit", |
| 117 | ); |
| 118 | write("docs/guide.md", b"# Guide\n\n[Back](../README.md)\n"); |
| 119 | write("docs/README", b"Plain docs readme <b>not html</b>\n"); |
| 120 | write("page.html", b"<html><script>alert(1)</script></html>\n"); |
| 121 | write("img/logo.png", b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR"); |
| 122 | write("data.bin", b"\0\x01\x02binary\0"); |
| 123 | write("big.txt", &b"0123456789abcdef\n".repeat(200)); // 3400 bytes |
| 124 | write("ünïcode dir/fïle #1.txt", b"unicode\n"); |
| 125 | std::os::unix::fs::symlink("src/main.rs", w.join("link-to-main")).unwrap(); |
| 126 | git(w, &["add", "-A"]); |
| 127 | git( |
| 128 | w, |
| 129 | &[ |
| 130 | "update-index", |
| 131 | "--add", |
| 132 | "--cacheinfo", |
| 133 | &format!("160000,{first},vendor/lib"), |
| 134 | ], |
| 135 | ); |
| 136 | git(w, &["commit", "-q", "-m", "add everything"]); |
| 137 | let head = git(w, &["rev-parse", "HEAD"]); |
| 138 | git(w, &["tag", "v1.0"]); |
| 139 | git( |
| 140 | w, |
| 141 | &["tag", "-a", "v1.1", "-m", "Release 1.1", first.as_str()], |
| 142 | ); |
| 143 | git(w, &["checkout", "-q", "-b", "feature/x"]); |
| 144 | let feature = work.commit("only-on-feature.txt", "feature\n", "feature work"); |
| 145 | git(w, &["checkout", "-q", "main"]); |
| 146 | git(w, &["push", "-q", "origin", "main", "feature/x", "--tags"]); |
| 147 | Rich { |
| 148 | bare, |
| 149 | head, |
| 150 | first, |
| 151 | feature, |
| 152 | } |
| 153 | } |