1use gix::prelude::ObjectIdExt;
12use gix::refs::transaction::PreviousValue;
13
14use crate::error::{Error, Result};
15use crate::git_utils;
16use crate::session::Session;
17
18#[must_use]
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct PushOutput {
25 pub success: bool,
27 pub non_fast_forward: bool,
29 pub up_to_date: bool,
31 pub remote_name: String,
33 pub remote_ref: String,
35 pub commit_oid: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum PushProgress {
42 CheckingLocalState,
44 Serializing,
46 SerializationSkipped,
48 RebasingLocal,
50 Pushing {
52 remote_name: String,
54 local_ref: String,
56 remote_ref: String,
58 },
59 FetchingRemote {
61 remote_name: String,
63 remote_ref: String,
65 },
66 HydratingRemoteTip,
68 MaterializingRemote,
70 SerializingMerged,
72 RebasingMerged,
74}
75
76pub fn push_once(session: &Session, remote: Option<&str>, now: i64) -> Result<PushOutput> {
102 push_once_with_progress(session, remote, now, |_| {})
103}
104
105pub fn push_once_with_progress(
120 session: &Session,
121 remote: Option<&str>,
122 now: i64,
123 mut progress: impl FnMut(PushProgress),
124) -> Result<PushOutput> {
125 let repo = &session.repo;
126 let ns = session.namespace();
127
128 let remote_name = git_utils::resolve_meta_remote(repo, remote)?;
129 let local_ref = session.local_ref();
130 let remote_refspec = format!("refs/{ns}/main");
131 let remote_tracking_ref = format!("refs/{ns}/remotes/main");
132
133 progress(PushProgress::CheckingLocalState);
134 let mut local_oid = peeled_ref_oid(repo, &local_ref);
135 let remote_oid = peeled_ref_oid(repo, &remote_tracking_ref);
136
137 if should_serialize_before_push(session, local_oid.as_ref(), remote_oid.as_ref())? {
138 progress(PushProgress::Serializing);
139 let _ = crate::serialize::run(session, now, false)?;
140 local_oid = peeled_ref_oid(repo, &local_ref);
141 } else {
142 progress(PushProgress::SerializationSkipped);
143 }
144
145 if local_oid.is_none() {
147 return Err(Error::Other(
148 "nothing to push (no local metadata ref)".into(),
149 ));
150 }
151
152 if let (Some(local), Some(remote_id)) = (local_oid.as_ref(), remote_oid.as_ref()) {
154 if local == remote_id {
155 return Ok(PushOutput {
156 success: true,
157 non_fast_forward: false,
158 up_to_date: true,
159 remote_name,
160 remote_ref: remote_refspec,
161 commit_oid: local.to_string(),
162 });
163 }
164
165 progress(PushProgress::RebasingLocal);
166 rebase_local_on_remote(repo, &local_ref, &remote_tracking_ref)?;
167 local_oid = repo
168 .find_reference(&local_ref)
169 .ok()
170 .and_then(|r| r.into_fully_peeled_id().ok())
171 .map(gix::Id::detach);
172 }
173
174 let commit_oid_str = local_oid
175 .as_ref()
176 .map(ToString::to_string)
177 .unwrap_or_default();
178
179 let push_refspec = format!("{local_ref}:{remote_refspec}");
181 progress(PushProgress::Pushing {
182 remote_name: remote_name.clone(),
183 local_ref: local_ref.clone(),
184 remote_ref: remote_refspec.clone(),
185 });
186 let result = git_utils::run_git(repo, &["push", &remote_name, &push_refspec]);
187
188 match result {
189 Ok(_) => Ok(PushOutput {
190 success: true,
191 non_fast_forward: false,
192 up_to_date: false,
193 remote_name,
194 remote_ref: remote_refspec,
195 commit_oid: commit_oid_str,
196 }),
197 Err(e) => {
198 let err_msg = e.to_string();
199 let is_non_ff = err_msg.contains("non-fast-forward")
200 || err_msg.contains("rejected")
201 || err_msg.contains("fetch first");
202
203 if is_non_ff {
204 Ok(PushOutput {
205 success: false,
206 non_fast_forward: true,
207 up_to_date: false,
208 remote_name,
209 remote_ref: remote_refspec,
210 commit_oid: commit_oid_str,
211 })
212 } else {
213 Err(Error::GitCommand(format!("push failed: {err_msg}")))
214 }
215 }
216 }
217}
218
219fn peeled_ref_oid(repo: &gix::Repository, ref_name: &str) -> Option<gix::ObjectId> {
220 repo.find_reference(ref_name)
221 .ok()
222 .and_then(|r| r.into_fully_peeled_id().ok())
223 .map(gix::Id::detach)
224}
225
226fn should_serialize_before_push(
227 session: &Session,
228 local_oid: Option<&gix::ObjectId>,
229 remote_oid: Option<&gix::ObjectId>,
230) -> Result<bool> {
231 let Some(local) = local_oid else {
232 return Ok(true);
233 };
234
235 if remote_oid.is_some_and(|remote| remote == local) {
236 return Ok(true);
237 }
238
239 let Some(last_materialized) = session.store.get_last_materialized()? else {
240 return Ok(true);
241 };
242
243 Ok(!session
244 .store
245 .get_modified_since(last_materialized)?
246 .is_empty())
247}
248
249pub fn resolve_push_conflict(session: &Session, remote: Option<&str>, now: i64) -> Result<()> {
269 resolve_push_conflict_with_progress(session, remote, now, |_| {})
270}
271
272pub fn resolve_push_conflict_with_progress(
287 session: &Session,
288 remote: Option<&str>,
289 now: i64,
290 mut progress: impl FnMut(PushProgress),
291) -> Result<()> {
292 let repo = &session.repo;
293 let ns = session.namespace();
294
295 let remote_name = git_utils::resolve_meta_remote(repo, remote)?;
296 let local_ref = session.local_ref();
297 let remote_refspec = format!("refs/{ns}/main");
298 let remote_tracking_ref = format!("refs/{ns}/remotes/main");
299
300 let fetch_refspec = format!("{remote_refspec}:{remote_tracking_ref}");
302 progress(PushProgress::FetchingRemote {
303 remote_name: remote_name.clone(),
304 remote_ref: remote_refspec,
305 });
306 git_utils::run_git(repo, &["fetch", &remote_name, &fetch_refspec])?;
307
308 let short_ref = format!("{ns}/remotes/main");
310 progress(PushProgress::HydratingRemoteTip);
311 git_utils::hydrate_tip_blobs(repo, &remote_name, &short_ref)?;
312
313 progress(PushProgress::MaterializingRemote);
315 let _ = crate::materialize::run(session, None, now)?;
316
317 progress(PushProgress::SerializingMerged);
319 let _ = crate::serialize::run(session, now, false)?;
320
321 progress(PushProgress::RebasingMerged);
325 rebase_local_on_remote(repo, &local_ref, &remote_tracking_ref)?;
326
327 Ok(())
328}
329
330fn rebase_local_on_remote(repo: &gix::Repository, local_ref: &str, remote_ref: &str) -> Result<()> {
336 let local_ref_obj = repo
337 .find_reference(local_ref)
338 .map_err(|e| Error::Other(format!("{e}")))?;
339 let local_oid = local_ref_obj
340 .into_fully_peeled_id()
341 .map_err(|e| Error::Other(format!("{e}")))?
342 .detach();
343 let local_commit_obj = local_oid
344 .attach(repo)
345 .object()
346 .map_err(|e| Error::Other(format!("{e}")))?
347 .into_commit();
348 let local_decoded = local_commit_obj
349 .decode()
350 .map_err(|e| Error::Other(format!("{e}")))?;
351
352 let remote_ref_obj = repo
353 .find_reference(remote_ref)
354 .map_err(|e| Error::Other(format!("{e}")))?;
355 let remote_oid = remote_ref_obj
356 .into_fully_peeled_id()
357 .map_err(|e| Error::Other(format!("{e}")))?
358 .detach();
359
360 let parent_ids: Vec<gix::ObjectId> = local_decoded.parents().collect();
362 if parent_ids.len() == 1 && parent_ids[0] == remote_oid {
363 return Ok(());
364 }
365
366 let tree_id = local_decoded.tree();
367 let message = local_decoded.message.to_owned();
368 let author_ref = local_decoded
369 .author()
370 .map_err(|e| Error::Other(format!("{e}")))?;
371
372 let commit = gix::objs::Commit {
373 message,
374 tree: tree_id,
375 author: gix::actor::Signature {
376 name: author_ref.name.into(),
377 email: author_ref.email.into(),
378 time: author_ref
379 .time()
380 .map_err(|e| Error::Other(format!("{e}")))?,
381 },
382 committer: gix::actor::Signature {
383 name: author_ref.name.into(),
384 email: author_ref.email.into(),
385 time: author_ref
386 .time()
387 .map_err(|e| Error::Other(format!("{e}")))?,
388 },
389 encoding: None,
390 parents: vec![remote_oid].into(),
391 extra_headers: Default::default(),
392 };
393
394 let new_oid = repo
395 .write_object(&commit)
396 .map_err(|e| Error::Other(format!("{e}")))?
397 .detach();
398 repo.reference(
399 local_ref,
400 new_oid,
401 PreviousValue::Any,
402 "git-meta: rebase for push",
403 )
404 .map_err(|e| Error::Other(format!("{e}")))?;
405
406 Ok(())
407}