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())? {
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) -> Result<bool> {
230 if local_oid.is_none() {
231 return Ok(true);
232 };
233
234 let Some(last_materialized) = session.store.get_last_materialized()? else {
235 return Ok(true);
236 };
237
238 Ok(!session
239 .store
240 .get_modified_since(last_materialized)?
241 .is_empty())
242}
243
244pub fn resolve_push_conflict(session: &Session, remote: Option<&str>, now: i64) -> Result<()> {
264 resolve_push_conflict_with_progress(session, remote, now, |_| {})
265}
266
267pub fn resolve_push_conflict_with_progress(
282 session: &Session,
283 remote: Option<&str>,
284 now: i64,
285 mut progress: impl FnMut(PushProgress),
286) -> Result<()> {
287 let repo = &session.repo;
288 let ns = session.namespace();
289
290 let remote_name = git_utils::resolve_meta_remote(repo, remote)?;
291 let local_ref = session.local_ref();
292 let remote_refspec = format!("refs/{ns}/main");
293 let remote_tracking_ref = format!("refs/{ns}/remotes/main");
294
295 let fetch_refspec = format!("{remote_refspec}:{remote_tracking_ref}");
297 progress(PushProgress::FetchingRemote {
298 remote_name: remote_name.clone(),
299 remote_ref: remote_refspec,
300 });
301 git_utils::run_git(repo, &["fetch", &remote_name, &fetch_refspec])?;
302
303 let short_ref = format!("{ns}/remotes/main");
305 progress(PushProgress::HydratingRemoteTip);
306 git_utils::hydrate_tip_blobs(repo, &remote_name, &short_ref)?;
307
308 progress(PushProgress::MaterializingRemote);
310 let _ = crate::materialize::run(session, None, now)?;
311
312 progress(PushProgress::SerializingMerged);
314 let _ = crate::serialize::run(session, now, false)?;
315
316 progress(PushProgress::RebasingMerged);
320 rebase_local_on_remote(repo, &local_ref, &remote_tracking_ref)?;
321
322 Ok(())
323}
324
325fn rebase_local_on_remote(repo: &gix::Repository, local_ref: &str, remote_ref: &str) -> Result<()> {
331 let local_ref_obj = repo
332 .find_reference(local_ref)
333 .map_err(|e| Error::Other(format!("{e}")))?;
334 let local_oid = local_ref_obj
335 .into_fully_peeled_id()
336 .map_err(|e| Error::Other(format!("{e}")))?
337 .detach();
338 let local_commit_obj = local_oid
339 .attach(repo)
340 .object()
341 .map_err(|e| Error::Other(format!("{e}")))?
342 .into_commit();
343 let local_decoded = local_commit_obj
344 .decode()
345 .map_err(|e| Error::Other(format!("{e}")))?;
346
347 let remote_ref_obj = repo
348 .find_reference(remote_ref)
349 .map_err(|e| Error::Other(format!("{e}")))?;
350 let remote_oid = remote_ref_obj
351 .into_fully_peeled_id()
352 .map_err(|e| Error::Other(format!("{e}")))?
353 .detach();
354
355 let parent_ids: Vec<gix::ObjectId> = local_decoded.parents().collect();
357 if parent_ids.len() == 1 && parent_ids[0] == remote_oid {
358 return Ok(());
359 }
360
361 let tree_id = local_decoded.tree();
362 let message = local_decoded.message.to_owned();
363 let author_ref = local_decoded
364 .author()
365 .map_err(|e| Error::Other(format!("{e}")))?;
366
367 let commit = gix::objs::Commit {
368 message,
369 tree: tree_id,
370 author: gix::actor::Signature {
371 name: author_ref.name.into(),
372 email: author_ref.email.into(),
373 time: author_ref
374 .time()
375 .map_err(|e| Error::Other(format!("{e}")))?,
376 },
377 committer: gix::actor::Signature {
378 name: author_ref.name.into(),
379 email: author_ref.email.into(),
380 time: author_ref
381 .time()
382 .map_err(|e| Error::Other(format!("{e}")))?,
383 },
384 encoding: None,
385 parents: vec![remote_oid].into(),
386 extra_headers: Default::default(),
387 };
388
389 let new_oid = repo
390 .write_object(&commit)
391 .map_err(|e| Error::Other(format!("{e}")))?
392 .detach();
393 repo.reference(
394 local_ref,
395 new_oid,
396 PreviousValue::Any,
397 "git-meta: rebase for push",
398 )
399 .map_err(|e| Error::Other(format!("{e}")))?;
400
401 Ok(())
402}
403
404#[cfg(test)]
405#[allow(clippy::expect_used, clippy::unwrap_used)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn clean_store_with_local_ref_does_not_need_serialization() {
411 let dir = tempfile::TempDir::new().unwrap();
412 let _repo = gix::init(dir.path()).unwrap();
413 let status = std::process::Command::new("git")
414 .args(["config", "user.email", "test@example.com"])
415 .current_dir(dir.path())
416 .status()
417 .unwrap();
418 assert!(status.success());
419 let status = std::process::Command::new("git")
420 .args(["config", "user.name", "Test User"])
421 .current_dir(dir.path())
422 .status()
423 .unwrap();
424 assert!(status.success());
425
426 let session = Session::open(dir.path()).unwrap();
427 session.store.set_last_materialized(1000).unwrap();
428 let local_oid =
429 gix::ObjectId::from_hex(b"0000000000000000000000000000000000000000").unwrap();
430
431 assert!(!should_serialize_before_push(&session, Some(&local_oid)).unwrap());
432 }
433}