upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/git.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2023-10-01 00:00:00 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2023-10-01 00:00:00 +0100
commit6e9245542f070c39a1975f0d53d88913c4ac667d (patch)
tree835c1d3db05f76f437c5d8ebc5591f1796cdab60 /src/git.rs
parentaa48a626c08cec353d5563a8831239d2e69c9f3d (diff)
feat(prs-create) find commits and create events
- identify commits - create pull request event - create patch events
Diffstat (limited to 'src/git.rs')
-rw-r--r--src/git.rs476
1 files changed, 476 insertions, 0 deletions
diff --git a/src/git.rs b/src/git.rs
new file mode 100644
index 0000000..ddbc646
--- /dev/null
+++ b/src/git.rs
@@ -0,0 +1,476 @@
1use std::env::current_dir;
2#[cfg(test)]
3use std::path::PathBuf;
4
5use anyhow::{bail, Context, Result};
6use git2::{Oid, Revwalk};
7use nostr::prelude::{sha1::Hash as Sha1Hash, Hash};
8
9pub struct Repo {
10 git_repo: git2::Repository,
11}
12
13impl Repo {
14 pub fn discover() -> Result<Self> {
15 Ok(Self {
16 git_repo: git2::Repository::discover(current_dir()?)?,
17 })
18 }
19 #[cfg(test)]
20 pub fn from_path(path: &PathBuf) -> Result<Self> {
21 Ok(Self {
22 git_repo: git2::Repository::open(path)?,
23 })
24 }
25}
26
27// pub type CommitId = [u8; 7];
28// pub type Sha1 = [u8; 20];
29
30pub trait RepoActions {
31 fn get_local_branch_names(&self) -> Result<Vec<String>>;
32 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)>;
33 fn get_tip_of_local_branch(&self, branch_name: &str) -> Result<Sha1Hash>;
34 fn get_root_commit(&self, branch_name: &str) -> Result<Sha1Hash>;
35 fn get_head_commit(&self) -> Result<Sha1Hash>;
36 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash>;
37 fn get_commits_ahead_behind(
38 &self,
39 base_commit: &Sha1Hash,
40 latest_commit: &Sha1Hash,
41 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)>;
42 fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String>;
43}
44
45impl RepoActions for Repo {
46 fn get_main_or_master_branch(&self) -> Result<(&str, Sha1Hash)> {
47 let main_branch_name = {
48 let local_branches = self
49 .get_local_branch_names()
50 .context("cannot find any local branches")?;
51 if local_branches.contains(&"main".to_string()) {
52 "main"
53 } else if local_branches.contains(&"master".to_string()) {
54 "master"
55 } else {
56 bail!("no main or master branch locally in this git repository to initiate from",)
57 }
58 };
59
60 let tip = self
61 .get_tip_of_local_branch(main_branch_name)
62 .context(format!(
63 "branch {main_branch_name} was listed as a local branch but cannot get its tip commit id",
64 ))?;
65
66 Ok((main_branch_name, tip))
67 }
68
69 fn get_local_branch_names(&self) -> Result<Vec<String>> {
70 let local_branches = self
71 .git_repo
72 .branches(Some(git2::BranchType::Local))
73 .context("getting GitRepo branches should not error even for a blank repository")?;
74
75 let mut branch_names = vec![];
76
77 for iter in local_branches {
78 let branch = iter?.0;
79 if let Some(name) = branch.name()? {
80 branch_names.push(name.to_string());
81 }
82 }
83 Ok(branch_names)
84 }
85
86 fn get_tip_of_local_branch(&self, branch_name: &str) -> Result<Sha1Hash> {
87 let branch = self
88 .git_repo
89 .find_branch(branch_name, git2::BranchType::Local)
90 .context(format!("cannot find branch {branch_name}"))?;
91 Ok(oid_to_sha1(&branch.into_reference().peel_to_commit()?.id()))
92 }
93
94 fn get_root_commit(&self, branch_name: &str) -> Result<Sha1Hash> {
95 let tip = self.get_tip_of_local_branch(branch_name)?;
96 let mut revwalk = self
97 .git_repo
98 .revwalk()
99 .context("revwalk should be created from git repo")?;
100 revwalk
101 .push(sha1_to_oid(&tip)?)
102 .context("revwalk should accept tip oid")?;
103 Ok(oid_to_sha1(
104 &revwalk
105 .last()
106 .context("revwalk from tip should be at least contain the tip oid")?
107 .context("revwalk iter from branch tip should not result in an error")?,
108 ))
109 }
110
111 fn get_head_commit(&self) -> Result<Sha1Hash> {
112 let head = self
113 .git_repo
114 .head()
115 .context("failed to get git repo head")?;
116 let oid = head.peel_to_commit()?.id();
117 Ok(oid_to_sha1(&oid))
118 }
119
120 fn get_commit_parent(&self, commit: &Sha1Hash) -> Result<Sha1Hash> {
121 let parent_oid = self
122 .git_repo
123 .find_commit(sha1_to_oid(commit)?)
124 .context(format!("could not find commit {commit}"))?
125 .parent_id(0)
126 .context(format!("could not find parent of commit {commit}"))?;
127 Ok(oid_to_sha1(&parent_oid))
128 }
129
130 fn make_patch_from_commit(&self, commit: &Sha1Hash) -> Result<String> {
131 let c = self
132 .git_repo
133 .find_commit(Oid::from_bytes(commit.as_byte_array()).context(format!(
134 "failed to convert commit_id format for {}",
135 &commit
136 ))?)
137 .context(format!("failed to find commit {}", &commit))?;
138 let patch = git2::Email::from_commit(&c, &mut git2::EmailCreateOptions::default())
139 .context(format!("failed to create patch from commit {}", &commit))?;
140
141 Ok(std::str::from_utf8(patch.as_slice())
142 .context("patch content could not be converted to a utf8 string")?
143 .to_owned())
144 }
145
146 fn get_commits_ahead_behind(
147 &self,
148 base_commit: &Sha1Hash,
149 latest_commit: &Sha1Hash,
150 ) -> Result<(Vec<Sha1Hash>, Vec<Sha1Hash>)> {
151 let mut ahead: Vec<Sha1Hash> = vec![];
152 let mut behind: Vec<Sha1Hash> = vec![];
153
154 let get_revwalk = |commit: &Sha1Hash| -> Result<Revwalk> {
155 let mut revwalk = self
156 .git_repo
157 .revwalk()
158 .context("revwalk should be created from git repo")?;
159 revwalk
160 .push(sha1_to_oid(commit)?)
161 .context("revwalk should accept commit oid")?;
162 Ok(revwalk)
163 };
164
165 // scan through the base commit ancestory until a common ancestor is found
166 let most_recent_shared_commit = match get_revwalk(base_commit)
167 .context("failed to get revwalk for base_commit")?
168 .find(|base_res| {
169 let base_oid = base_res.as_ref().unwrap();
170
171 if get_revwalk(latest_commit)
172 .unwrap()
173 .any(|latest_res| base_oid.eq(latest_res.as_ref().unwrap()))
174 {
175 true
176 } else {
177 // add commits not found in latest ancestory to 'behind' vector
178 behind.push(oid_to_sha1(base_oid));
179 false
180 }
181 }) {
182 None => {
183 bail!(format!(
184 "{} is not an ancestor of {}",
185 latest_commit, base_commit
186 ));
187 }
188 Some(res) => res.context("revwalk failed to reveal commit")?,
189 };
190
191 // scan through the latest commits until shared commit is reached
192 get_revwalk(latest_commit)
193 .context("failed to get revwalk for latest_commit")?
194 .any(|latest_res| {
195 let latest_oid = latest_res.as_ref().unwrap();
196 if latest_oid.eq(&most_recent_shared_commit) {
197 true
198 } else {
199 // add commits not found in base to 'ahead' vector
200 ahead.push(oid_to_sha1(latest_oid));
201 false
202 }
203 });
204 Ok((ahead, behind))
205 }
206}
207
208fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] {
209 let b = oid.as_bytes();
210 [
211 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13],
212 b[14], b[15], b[16], b[17], b[18], b[19],
213 ]
214}
215
216// fn oid_to_shorthand_string(oid: Oid) -> Result<String> {
217// let binding = oid.to_string();
218// let b = binding.as_bytes();
219// String::from_utf8(vec![b[0], b[1], b[2], b[3], b[4], b[5], b[6]])
220// .context("oid should always start with 7 u8 btyes of utf8")
221// }
222
223// fn oid_to_sha1_string(oid: Oid) -> Result<String> {
224// let b = oid.as_bytes();
225// String::from_utf8(vec![
226// b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10],
227// b[11], b[12], b[13], b[14], b[15], b[16], b[17], b[18], b[19],
228// ])
229// .context("oid should contain 20 u8 btyes of utf8")
230// }
231
232// git2 Oid object to Sha1Hash
233pub fn oid_to_sha1(oid: &Oid) -> Sha1Hash {
234 Sha1Hash::from_byte_array(oid_to_u8_20_bytes(oid))
235}
236
237/// `Sha1Hash` to git2 `Oid` object
238fn sha1_to_oid(hash: &Sha1Hash) -> Result<Oid> {
239 Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid")
240}
241
242#[cfg(test)]
243mod tests {
244 use test_utils::git::GitTestRepo;
245
246 use super::*;
247
248 mod make_patch_from_commit {
249 use super::*;
250 #[test]
251 fn simple_patch_matches_string() -> Result<()> {
252 let test_repo = GitTestRepo::default();
253 let oid = test_repo.populate()?;
254
255 let git_repo = Repo::from_path(&test_repo.dir)?;
256
257 assert_eq!(
258 "\
259 From 431b84edc0d2fa118d63faa3c2db9c73d630a5ae Mon Sep 17 00:00:00 2001\n\
260 From: Joe Bloggs <joe.bloggs@pm.me>\n\
261 Date: Thu, 1 Jan 1970 00:00:00 +0000\n\
262 Subject: [PATCH] add t2.md\n\
263 \n\
264 ---\n \
265 t2.md | 1 +\n \
266 1 file changed, 1 insertion(+)\n \
267 create mode 100644 t2.md\n\
268 \n\
269 diff --git a/t2.md b/t2.md\n\
270 new file mode 100644\n\
271 index 0000000..a66525d\n\
272 --- /dev/null\n\
273 +++ b/t2.md\n\
274 @@ -0,0 +1 @@\n\
275 +some content1\n\\ \
276 No newline at end of file\n\
277 --\n\
278 libgit2 1.7.1\n\
279 \n\
280 ",
281 git_repo.make_patch_from_commit(&oid_to_sha1(&oid))?,
282 );
283 Ok(())
284 }
285 }
286
287 mod get_main_or_master_branch {
288
289 use super::*;
290 mod returns_main {
291 use super::*;
292 #[test]
293 fn when_it_exists() -> Result<()> {
294 let test_repo = GitTestRepo::new("main")?;
295 let main_oid = test_repo.populate()?;
296 let git_repo = Repo::from_path(&test_repo.dir)?;
297 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
298 assert_eq!(name, "main");
299 assert_eq!(commit_hash, oid_to_sha1(&main_oid));
300 Ok(())
301 }
302
303 #[test]
304 fn when_it_exists_and_other_branch_checkedout() -> Result<()> {
305 let test_repo = GitTestRepo::new("main")?;
306 let main_oid = test_repo.populate()?;
307 test_repo.create_branch("feature")?;
308 test_repo.checkout("feature")?;
309 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
310 let feature_oid = test_repo.stage_and_commit("add t3.md")?;
311
312 let git_repo = Repo::from_path(&test_repo.dir)?;
313 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
314 assert_eq!(name, "main");
315 assert_eq!(commit_hash, oid_to_sha1(&main_oid));
316 assert_ne!(commit_hash, oid_to_sha1(&feature_oid));
317 Ok(())
318 }
319
320 #[test]
321 fn when_exists_even_if_master_is_checkedout() -> Result<()> {
322 let test_repo = GitTestRepo::new("main")?;
323 let main_oid = test_repo.populate()?;
324 test_repo.create_branch("master")?;
325 test_repo.checkout("master")?;
326 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
327 let master_oid = test_repo.stage_and_commit("add t3.md")?;
328
329 let git_repo = Repo::from_path(&test_repo.dir)?;
330 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
331 assert_eq!(name, "main");
332 assert_eq!(commit_hash, oid_to_sha1(&main_oid));
333 assert_ne!(commit_hash, oid_to_sha1(&master_oid));
334 Ok(())
335 }
336 }
337
338 #[test]
339 fn returns_master_if_exists_and_main_doesnt() -> Result<()> {
340 let test_repo = GitTestRepo::new("master")?;
341 let master_oid = test_repo.populate()?;
342 test_repo.create_branch("feature")?;
343 test_repo.checkout("feature")?;
344 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
345 let feature_oid = test_repo.stage_and_commit("add t3.md")?;
346
347 let git_repo = Repo::from_path(&test_repo.dir)?;
348 let (name, commit_hash) = git_repo.get_main_or_master_branch()?;
349 assert_eq!(name, "master");
350 assert_eq!(commit_hash, oid_to_sha1(&master_oid));
351 assert_ne!(commit_hash, oid_to_sha1(&feature_oid));
352 Ok(())
353 }
354 #[test]
355 fn returns_error_if_no_main_or_master() -> Result<()> {
356 let test_repo = GitTestRepo::new("feature")?;
357 test_repo.populate()?;
358 let git_repo = Repo::from_path(&test_repo.dir)?;
359 assert!(git_repo.get_main_or_master_branch().is_err());
360 Ok(())
361 }
362 }
363
364 mod get_commits_ahead_behind {
365 use super::*;
366 mod returns_main {
367 use super::*;
368
369 #[test]
370 fn when_on_same_commit_return_empty() -> Result<()> {
371 let test_repo = GitTestRepo::default();
372 let oid = test_repo.populate()?;
373 // create feature branch
374 test_repo.create_branch("feature")?;
375 test_repo.checkout("feature")?;
376
377 let git_repo = Repo::from_path(&test_repo.dir)?;
378
379 let (ahead, behind) =
380 git_repo.get_commits_ahead_behind(&oid_to_sha1(&oid), &oid_to_sha1(&oid))?;
381 assert_eq!(ahead, vec![]);
382 assert_eq!(behind, vec![]);
383 Ok(())
384 }
385
386 #[test]
387 fn when_2_commit_behind() -> Result<()> {
388 let test_repo = GitTestRepo::default();
389 test_repo.populate()?;
390 // create feature branch
391 test_repo.create_branch("feature")?;
392 let feature_oid = test_repo.checkout("feature")?;
393 // checkout main and add 2 commits
394 test_repo.checkout("main")?;
395 std::fs::write(test_repo.dir.join("t5.md"), "some content")?;
396 let behind_1_oid = test_repo.stage_and_commit("add t5.md")?;
397 std::fs::write(test_repo.dir.join("t6.md"), "some content")?;
398 let behind_2_oid = test_repo.stage_and_commit("add t6.md")?;
399
400 let git_repo = Repo::from_path(&test_repo.dir)?;
401
402 let (ahead, behind) = git_repo.get_commits_ahead_behind(
403 &oid_to_sha1(&behind_2_oid),
404 &oid_to_sha1(&feature_oid),
405 )?;
406 assert_eq!(ahead, vec![]);
407 assert_eq!(
408 behind,
409 vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid),],
410 );
411 Ok(())
412 }
413
414 #[test]
415 fn when_2_commit_ahead() -> Result<()> {
416 let test_repo = GitTestRepo::default();
417 let main_oid = test_repo.populate()?;
418 // create feature branch and add 2 commits
419 test_repo.create_branch("feature")?;
420 test_repo.checkout("feature")?;
421 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
422 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
423 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
424 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
425
426 let git_repo = Repo::from_path(&test_repo.dir)?;
427
428 let (ahead, behind) = git_repo.get_commits_ahead_behind(
429 &oid_to_sha1(&main_oid),
430 &oid_to_sha1(&ahead_2_oid),
431 )?;
432 assert_eq!(
433 ahead,
434 vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid),],
435 );
436 assert_eq!(behind, vec![]);
437 Ok(())
438 }
439
440 #[test]
441 fn when_2_commit_ahead_and_2_commits_behind() -> Result<()> {
442 let test_repo = GitTestRepo::default();
443 test_repo.populate()?;
444 // create feature branch and add 2 commits
445 test_repo.create_branch("feature")?;
446 test_repo.checkout("feature")?;
447 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
448 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
449 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
450 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
451 // checkout main and add 2 commits
452 test_repo.checkout("main")?;
453 std::fs::write(test_repo.dir.join("t5.md"), "some content")?;
454 let behind_1_oid = test_repo.stage_and_commit("add t5.md")?;
455 std::fs::write(test_repo.dir.join("t6.md"), "some content")?;
456 let behind_2_oid = test_repo.stage_and_commit("add t6.md")?;
457
458 let git_repo = Repo::from_path(&test_repo.dir)?;
459
460 let (ahead, behind) = git_repo.get_commits_ahead_behind(
461 &oid_to_sha1(&behind_2_oid),
462 &oid_to_sha1(&ahead_2_oid),
463 )?;
464 assert_eq!(
465 ahead,
466 vec![oid_to_sha1(&ahead_2_oid), oid_to_sha1(&ahead_1_oid)],
467 );
468 assert_eq!(
469 behind,
470 vec![oid_to_sha1(&behind_2_oid), oid_to_sha1(&behind_1_oid)],
471 );
472 Ok(())
473 }
474 }
475 }
476}