1use super::ForEachRefTrack;
4use sley_config::GitConfig;
5use sley_core::{ObjectId, ObjectFormat, Result};
6use sley_odb::{FileObjectDatabase, ObjectReader};
7use sley_refs::{FileRefStore, Ref, RefTarget, validate_ref_name};
8use std::path::Path;
9
10#[derive(Clone)]
11pub struct ForEachRefUpstream {
12 pub refname: String,
13 pub remote: String,
14 pub merge: String,
15}
16
17#[derive(Clone)]
18pub struct ForEachRefPush {
19 pub refname: Option<String>,
20 pub remote: String,
21 pub remote_ref: Option<String>,
22}
23
24pub struct ForEachRefPushRemote {
25 name: String,
26 expose_name: bool,
27}
28
29pub fn for_each_ref_upstream(
30 config: &GitConfig,
31 refname: &str,
32) -> Option<ForEachRefUpstream> {
33 let branch = refname.strip_prefix("refs/heads/")?;
34 let remote = config.get("branch", Some(branch), "remote")?;
35 let merge = config.get("branch", Some(branch), "merge")?;
36 if remote == "." {
37 let refname = expand_local_upstream_merge(merge);
43 return Some(ForEachRefUpstream {
44 refname,
45 remote: remote.to_string(),
46 merge: merge.to_string(),
47 });
48 }
49 let fetch = config.get("remote", Some(remote), "fetch")?;
50 Some(ForEachRefUpstream {
51 refname: map_remote_fetch_refspec(fetch, merge)?,
52 remote: remote.to_string(),
53 merge: merge.to_string(),
54 })
55}
56
57pub fn expand_local_upstream_merge(merge: &str) -> String {
60 if merge.starts_with("refs/") {
61 merge.to_string()
62 } else {
63 format!("refs/heads/{merge}")
64 }
65}
66
67pub fn for_each_ref_push(config: &GitConfig, refname: &str) -> Option<ForEachRefPush> {
68 let branch = refname.strip_prefix("refs/heads/")?;
69 let push_remote = for_each_ref_push_remote(config, branch)?;
70 let remote_name = push_remote.name.clone();
71 let display_remote = remote_display_name(push_remote);
75 if remote_name == "." {
76 return Some(ForEachRefPush {
77 refname: None,
78 remote: display_remote,
79 remote_ref: None,
80 });
81 }
82 if let Some(push) = config.get("remote", Some(remote_name.as_str()), "push") {
85 if let Some(remote_ref) = map_remote_push_refspec(push, refname) {
86 let tracking = map_remote_tracking_ref(config, &remote_name, &remote_ref);
87 return Some(ForEachRefPush {
88 refname: tracking,
89 remote: display_remote,
90 remote_ref: Some(remote_ref),
91 });
92 }
93 return Some(ForEachRefPush {
94 refname: None,
95 remote: display_remote,
96 remote_ref: None,
97 });
98 }
99 let push_default = config.get("push", None, "default").unwrap_or("simple");
102 let tracking = match push_default {
103 "nothing" => None,
104 "matching" | "current" => map_remote_tracking_ref(config, &remote_name, refname),
107 "upstream" => for_each_ref_upstream(config, refname).map(|up| up.refname),
109 _ => {
113 let up = for_each_ref_upstream(config, refname).map(|up| up.refname);
114 let cur = map_remote_tracking_ref(config, &remote_name, refname);
115 match (up, cur) {
116 (Some(up), Some(cur)) if up == cur => Some(cur),
117 _ => None,
118 }
119 }
120 };
121 Some(ForEachRefPush {
122 refname: tracking,
123 remote: display_remote,
124 remote_ref: None,
125 })
126}
127
128pub fn for_each_ref_push_remote(
129 config: &GitConfig,
130 branch: &str,
131) -> Option<ForEachRefPushRemote> {
132 if let Some(remote) = config.get("branch", Some(branch), "pushRemote") {
133 return Some(ForEachRefPushRemote {
134 name: remote.to_string(),
135 expose_name: true,
136 });
137 }
138 if let Some(remote) = config.get("remote", None, "pushDefault") {
139 return Some(ForEachRefPushRemote {
140 name: remote.to_string(),
141 expose_name: true,
142 });
143 }
144 if let Some(remote) = config.get("branch", Some(branch), "remote") {
145 return Some(ForEachRefPushRemote {
146 name: remote.to_string(),
147 expose_name: true,
148 });
149 }
150 if sley_config::remotes::remote_exists(config, "origin") {
151 return Some(ForEachRefPushRemote {
152 name: "origin".to_string(),
153 expose_name: false,
154 });
155 }
156 let remotes = sley_config::remotes::remote_names(config);
157 match remotes.as_slice() {
158 [remote] => Some(ForEachRefPushRemote {
159 name: remote.clone(),
160 expose_name: false,
161 }),
162 _ => None,
163 }
164}
165
166pub fn remote_display_name(remote: ForEachRefPushRemote) -> String {
167 if remote.expose_name {
168 remote.name
169 } else {
170 String::new()
171 }
172}
173
174pub fn map_remote_tracking_ref(
175 config: &GitConfig,
176 remote: &str,
177 remote_ref: &str,
178) -> Option<String> {
179 let fetch = config.get("remote", Some(remote), "fetch")?;
180 map_remote_fetch_refspec(fetch, remote_ref)
181}
182
183pub fn map_remote_push_refspec(refspec: &str, refname: &str) -> Option<String> {
184 let refspec = parse_refspec(refspec).ok()?;
185 if refspec.negative || refspec.src.is_none() || refspec.dst.is_none() {
186 return None;
187 }
188 refspec_map_source(&refspec, refname).ok()?
189}
190
191pub fn map_remote_fetch_refspec(refspec: &str, merge: &str) -> Option<String> {
192 let refspec = parse_refspec(refspec).ok()?;
193 if refspec.negative || refspec.dst.is_none() {
194 return None;
195 }
196 refspec_map_source(&refspec, merge).ok()?
197}
198
199use sley_protocol::parse_refspec;
200use sley_protocol::refspec_map_source;
201
202pub fn for_each_ref_upstream_track(
203 store: &FileRefStore,
204 git_dir: &Path,
205 db: &FileObjectDatabase,
206 format: ObjectFormat,
207 oid: &ObjectId,
208 upstream: &str,
209) -> Result<Option<ForEachRefTrack>> {
210 let gone_track = ForEachRefTrack {
213 ahead: 0,
214 behind: 0,
215 gone: true,
216 };
217 let Some(upstream_target) = store.read_ref(upstream)? else {
218 return Ok(Some(gone_track));
219 };
220 let upstream_ref = Ref {
221 name: upstream.to_string(),
222 target: upstream_target,
223 };
224 let Some((upstream_oid, _)) = resolve_for_each_ref_target(store, &upstream_ref)? else {
225 return Ok(Some(gone_track));
226 };
227 for_each_ref_ahead_behind(git_dir, db, format, oid, &upstream_oid)
228}
229
230pub fn for_each_ref_ahead_behind_with_diagnostic(
231 git_dir: &Path,
232 db: &FileObjectDatabase,
233 format: ObjectFormat,
234 oid: &ObjectId,
235 target: &ObjectId,
236) -> Result<Option<ForEachRefTrack>> {
237 let Ok(local_commit) = sley_rev::peel_to_commit(db, format, oid) else {
238 if let Ok(object) = db.read_object(oid) {
239 eprintln!(
240 "error: object {} is a {}, not a commit",
241 oid,
242 object.object_type.as_str()
243 );
244 }
245 return Ok(None);
246 };
247 let Ok(target_commit) = sley_rev::peel_to_commit(db, format, target) else {
248 return Ok(None);
249 };
250 let (ahead, behind) =
251 sley_rev::ahead_behind_counts(git_dir, format, db, &local_commit, &target_commit)?;
252 Ok(Some(ForEachRefTrack {
253 ahead,
254 behind,
255 gone: false,
256 }))
257}
258
259pub fn for_each_ref_ahead_behind(
260 git_dir: &Path,
261 db: &FileObjectDatabase,
262 format: ObjectFormat,
263 oid: &ObjectId,
264 target: &ObjectId,
265) -> Result<Option<ForEachRefTrack>> {
266 let Ok(local_commit) = sley_rev::peel_to_commit(db, format, oid) else {
267 return Ok(None);
268 };
269 let Ok(target_commit) = sley_rev::peel_to_commit(db, format, target) else {
270 return Ok(None);
271 };
272 let (ahead, behind) =
273 sley_rev::ahead_behind_counts(git_dir, format, db, &local_commit, &target_commit)?;
274 Ok(Some(ForEachRefTrack {
275 ahead,
276 behind,
277 gone: false,
278 }))
279}
280
281pub fn resolve_for_each_ref_target(
282 store: &FileRefStore,
283 reference: &Ref,
284) -> Result<Option<(ObjectId, Option<String>)>> {
285 let mut target = reference.target.clone();
286 let mut symref = None;
287 for _ in 0..5 {
288 match target {
289 RefTarget::Direct(oid) => return Ok(Some((oid, symref))),
290 RefTarget::Symbolic(name) => {
291 symref.get_or_insert_with(|| name.clone());
292 if validate_ref_name(&name).is_err() {
293 return Ok(None);
294 }
295 let Some(next) = store.read_ref(&name)? else {
296 return Ok(None);
297 };
298 target = next;
299 }
300 }
301 }
302 Ok(None)
303}