1use std::path::Path;
9
10use anyhow::{Context, Result};
11use git2::{Delta, Repository};
12
13#[derive(Debug, Default, Clone)]
15pub struct ChangeSet {
16 pub added: Vec<String>,
18 pub modified: Vec<String>,
20 pub deleted: Vec<String>,
22}
23
24impl ChangeSet {
25 pub fn total(&self) -> usize {
26 self.added.len() + self.modified.len() + self.deleted.len()
27 }
28
29 pub fn to_parse(&self) -> impl Iterator<Item = &String> {
31 self.added.iter().chain(self.modified.iter())
32 }
33
34 pub fn to_drop(&self) -> impl Iterator<Item = &String> {
36 self.modified.iter().chain(self.deleted.iter())
37 }
38}
39
40fn path_string(p: Option<&Path>) -> Option<String> {
41 p.map(|p| p.to_string_lossy().replace('\\', "/"))
42}
43
44pub fn head_commit(repo_path: &Path) -> Result<String> {
47 let repo = Repository::open(repo_path)
48 .with_context(|| format!("open git repo at {}", repo_path.display()))?;
49 let commit = repo.head()?.peel_to_commit()?;
50 Ok(commit.id().to_string())
51}
52
53pub fn changed_files(repo_path: &Path, base_sha: &str) -> Result<ChangeSet> {
56 let repo = Repository::open(repo_path)
57 .with_context(|| format!("open git repo at {}", repo_path.display()))?;
58 let base_tree = repo
59 .revparse_single(base_sha)
60 .with_context(|| format!("resolve base commit {base_sha}"))?
61 .peel_to_commit()?
62 .tree()?;
63 let head_tree = repo.head()?.peel_to_commit()?.tree()?;
64
65 let mut opts = git2::DiffOptions::new();
66 let diff = repo.diff_tree_to_tree(Some(&base_tree), Some(&head_tree), Some(&mut opts))?;
67
68 let mut cs = ChangeSet::default();
69 for d in diff.deltas() {
70 let new_p = path_string(d.new_file().path());
71 let old_p = path_string(d.old_file().path());
72 match d.status() {
73 Delta::Added | Delta::Copied => cs.added.extend(new_p),
74 Delta::Deleted => cs.deleted.extend(old_p),
75 Delta::Modified | Delta::Typechange => cs.modified.extend(new_p),
76 Delta::Renamed => {
77 cs.deleted.extend(old_p);
78 cs.added.extend(new_p);
79 }
80 _ => {}
81 }
82 }
83 Ok(cs)
84}
85
86#[derive(Debug, Clone)]
88pub struct CommitFiles {
89 pub sha: String,
90 pub subject: String,
91 pub added: Vec<String>,
92 pub changed: Vec<String>,
93}
94
95pub fn history(repo_path: &Path, limit: usize) -> Result<Vec<CommitFiles>> {
97 let repo = Repository::open(repo_path)
98 .with_context(|| format!("open git repo at {}", repo_path.display()))?;
99 let mut walk = repo.revwalk()?;
100 walk.push_head()?;
101 walk.simplify_first_parent()?;
102 let mut out = Vec::new();
103 for oid in walk.take(limit) {
104 let commit = repo.find_commit(oid?)?;
105 let tree = commit.tree()?;
106 let parent = commit.parents().next().map(|p| p.tree()).transpose()?;
107 let diff = repo.diff_tree_to_tree(parent.as_ref(), Some(&tree), None)?;
108 let mut c = CommitFiles {
109 sha: commit.id().to_string(),
110 subject: commit
111 .summary()
112 .ok()
113 .flatten()
114 .unwrap_or_default()
115 .to_string(),
116 added: Vec::new(),
117 changed: Vec::new(),
118 };
119 for d in diff.deltas() {
120 let Some(p) = path_string(d.new_file().path()) else {
121 continue;
122 };
123 if matches!(d.status(), Delta::Added | Delta::Copied | Delta::Renamed) {
124 c.added.push(p.clone());
125 }
126 if !matches!(d.status(), Delta::Deleted) {
127 c.changed.push(p);
128 }
129 }
130 out.push(c);
131 }
132 Ok(out)
133}
134
135pub type FileHunks = (String, Vec<(u32, u32)>);
137
138pub fn working_changes(repo_path: &Path, base: &str) -> Result<Vec<FileHunks>> {
140 let repo = Repository::open(repo_path)
141 .with_context(|| format!("open git repo at {}", repo_path.display()))?;
142 let tree = repo.revparse_single(base)?.peel_to_tree()?;
143 let mut opts = git2::DiffOptions::new();
144 opts.include_untracked(true).recurse_untracked_dirs(true);
145 let diff = repo.diff_tree_to_workdir_with_index(Some(&tree), Some(&mut opts))?;
146 let out: std::cell::RefCell<Vec<FileHunks>> = Default::default();
147 diff.foreach(
148 &mut |d, _| {
149 if let Some(p) = path_string(d.new_file().path()) {
150 out.borrow_mut().push((p, Vec::new()));
151 }
152 true
153 },
154 None,
155 Some(&mut |d, h| {
156 let mut v = out.borrow_mut();
157 if let (Some(p), Some(last)) = (path_string(d.new_file().path()), v.last_mut()) {
158 if last.0 == p {
159 let start = h.new_start().max(1);
160 last.1.push((start, start + h.new_lines().max(1) - 1));
161 }
162 }
163 true
164 }),
165 None,
166 )?;
167 Ok(out.into_inner())
168}