1use anyhow::anyhow;
2use josh_core::Change;
3
4#[derive(PartialEq, Clone, Debug)]
5pub enum PushMode {
6 Normal,
7 Stack(String),
8 Split(String),
9}
10
11#[derive(Debug, Clone)]
12pub struct PushRef {
13 pub ref_name: String,
14 pub oid: git2::Oid,
15 pub change_id: String,
16}
17
18pub fn baseref_and_options(
19 refname: &str,
20 author: &str,
21) -> anyhow::Result<(String, String, Vec<String>, PushMode)> {
22 let mut split = refname.splitn(2, '%');
23 let push_to = split.next().ok_or(anyhow!("no next"))?.to_owned();
24
25 let options = if let Some(options) = split.next() {
26 options.split(',').map(|x| x.to_string()).collect()
27 } else {
28 vec![]
29 };
30
31 let mut baseref = push_to.to_owned();
32 let mut push_mode = PushMode::Normal;
33
34 if baseref.starts_with("refs/for") {
35 baseref = baseref.replacen("refs/for", "refs/heads", 1)
36 }
37 if baseref.starts_with("refs/drafts") {
38 baseref = baseref.replacen("refs/drafts", "refs/heads", 1)
39 }
40 if baseref.starts_with("refs/stack/for") {
41 push_mode = PushMode::Stack(author.to_string());
42 baseref = baseref.replacen("refs/stack/for", "refs/heads", 1)
43 }
44 if baseref.starts_with("refs/split/for") {
45 push_mode = PushMode::Split(author.to_string());
46 baseref = baseref.replacen("refs/split/for", "refs/heads", 1)
47 }
48 Ok((baseref, push_to, options, push_mode))
49}
50
51fn add_base_refs(repo: &git2::Repository, refs: &mut Vec<PushRef>) -> anyhow::Result<()> {
52 let original_refs = std::mem::take(refs);
53 for push_ref in original_refs.into_iter() {
54 let base_ref = push_ref
55 .ref_name
56 .replacen("refs/heads/@changes", "refs/heads/@base", 1);
57
58 let oid = push_ref.oid;
59 let change_id = push_ref.change_id.clone();
60 refs.push(push_ref);
61
62 if let Some(parent_sha) = repo.find_commit(oid)?.parent_ids().next() {
63 refs.push(PushRef {
64 ref_name: base_ref,
65 oid: parent_sha,
66 change_id,
67 });
68 }
69 }
70
71 Ok(())
72}
73
74fn split_changes(
75 repo: &git2::Repository,
76 changes: &mut [PushRef],
77 base: git2::Oid,
78) -> anyhow::Result<()> {
79 if base == git2::Oid::zero() {
80 return Ok(());
81 }
82
83 for push_ref in changes.iter_mut() {
84 push_ref.oid = downstack(repo, base, push_ref.oid)?;
85 }
86
87 Ok(())
88}
89
90pub fn downstack(
91 repo: &git2::Repository,
92 base: git2::Oid,
93 change: git2::Oid,
94) -> anyhow::Result<git2::Oid> {
95 if !repo.graph_descendant_of(change, base)? {
96 return Err(anyhow!(
97 "change {} is not a descendant of base {}",
98 change,
99 base
100 ));
101 }
102
103 let mut walk = repo.revwalk()?;
105 walk.simplify_first_parent()?;
106 walk.set_sorting(git2::Sort::REVERSE | git2::Sort::TOPOLOGICAL)?;
107 walk.push(change)?;
108 walk.hide(base)?;
109
110 let oids: Vec<git2::Oid> = walk.collect::<Result<Vec<_>, _>>()?;
111
112 if oids.is_empty() {
113 return Ok(change);
114 }
115
116 let mut commits: Vec<git2::Commit> = oids
117 .into_iter()
118 .map(|oid| repo.find_commit(oid))
119 .collect::<Result<Vec<_>, _>>()?;
120
121 let change_commit = commits.pop().unwrap();
123 let change_parent = change_commit.parent(0)?;
124
125 let change_diff = repo.diff_tree_to_tree(
127 Some(&change_parent.tree()?),
128 Some(&change_commit.tree()?),
129 None,
130 )?;
131
132 let mut current_base = repo.find_commit(base)?;
134
135 for intermediate in &commits {
136 if repo
138 .apply_to_tree(¤t_base.tree()?, &change_diff, None)
139 .is_ok()
140 {
141 break;
142 }
143
144 let inter_parent = intermediate.parent(0)?;
147 let inter_diff = repo.diff_tree_to_tree(
148 Some(&inter_parent.tree()?),
149 Some(&intermediate.tree()?),
150 None,
151 )?;
152
153 let mut index = repo.apply_to_tree(¤t_base.tree()?, &inter_diff, None)?;
154 let new_tree = repo.find_tree(index.write_tree_to(repo)?)?;
155
156 let new_oid = josh_core::history::rewrite_commit(
157 repo,
158 intermediate,
159 &[¤t_base],
160 josh_core::filter::Rewrite::from_tree(new_tree),
161 josh_core::history::GpgsigMode::Preserve,
162 )?;
163 current_base = repo.find_commit(new_oid)?;
164 }
165
166 let mut index = repo.apply_to_tree(¤t_base.tree()?, &change_diff, None)?;
168 let new_tree = repo.find_tree(index.write_tree_to(repo)?)?;
169
170 josh_core::history::rewrite_commit(
171 repo,
172 &change_commit,
173 &[¤t_base],
174 josh_core::filter::Rewrite::from_tree(new_tree),
175 josh_core::history::GpgsigMode::Preserve,
176 )
177}
178
179pub fn changes_to_refs(
180 baseref: &str,
181 change_author: &str,
182 changes: Vec<Change>,
183) -> anyhow::Result<Vec<PushRef>> {
184 if !change_author.contains('@') {
185 return Err(anyhow!(
186 "Push option 'author' needs to be set to a valid email address",
187 ));
188 };
189
190 let changes: Vec<Change> = changes
191 .into_iter()
192 .filter(|change| change.author == change_author)
193 .collect();
194
195 let mut seen = std::collections::HashSet::new();
196 for change in changes.iter() {
197 if let Some(id) = &change.id {
198 if id.contains('@') {
199 return Err(anyhow!("Change id must not contain '@'"));
200 }
201 if !seen.insert(id) {
202 return Err(anyhow!(
203 "rejecting to push {:?} with duplicate label",
204 change.commit
205 ));
206 }
207 seen.insert(id);
208 }
209 }
210
211 Ok(changes
212 .into_iter()
213 .filter_map(|change| {
214 change.id.map(|change_id| PushRef {
215 ref_name: format!(
216 "refs/heads/@changes/{}/{}/{}",
217 baseref.replacen("refs/heads/", "", 1),
218 change.author,
219 change_id,
220 ),
221 oid: change.commit,
222 change_id,
223 })
224 })
225 .collect())
226}
227
228fn get_changes(
229 repo: &git2::Repository,
230 tip: git2::Oid,
231 base: git2::Oid,
232) -> anyhow::Result<Vec<Change>> {
233 let mut walk = repo.revwalk()?;
234 walk.set_sorting(git2::Sort::REVERSE | git2::Sort::TOPOLOGICAL)?;
235 walk.simplify_first_parent()?;
236 walk.push(tip)?;
237 if base != git2::Oid::zero() {
238 walk.hide(base)?;
239 }
240
241 let mut changes = vec![];
242 for rev in walk {
243 let commit = repo.find_commit(rev?)?;
244 changes.push(josh_core::get_change_id(&commit));
245 }
246
247 Ok(changes)
248}
249
250pub fn build_to_push(
251 repo: &git2::Repository,
252 push_mode: &PushMode,
253 baseref: &str,
254 ref_with_options: &str,
255 oid_to_push: git2::Oid,
256 base_oid: git2::Oid,
257) -> anyhow::Result<Vec<PushRef>> {
258 match push_mode {
259 PushMode::Stack(author) | PushMode::Split(author) => {
260 let changes = get_changes(repo, oid_to_push, base_oid)?;
261 let mut push_refs = changes_to_refs(baseref, author, changes)?;
262
263 if matches!(push_mode, PushMode::Split(_)) {
264 split_changes(repo, &mut push_refs, base_oid)?;
265 }
266
267 add_base_refs(repo, &mut push_refs)?;
268
269 push_refs.push(PushRef {
270 ref_name: format!(
271 "refs/heads/@heads/{}/{}",
272 baseref.replacen("refs/heads/", "", 1),
273 author,
274 ),
275 oid: oid_to_push,
276 change_id: baseref.replacen("refs/heads/", "", 1),
277 });
278
279 Ok(push_refs)
280 }
281 PushMode::Normal => Ok(vec![PushRef {
282 ref_name: ref_with_options.to_string(),
283 oid: oid_to_push,
284 change_id: "JOSH_PUSH".to_string(),
285 }]),
286 }
287}