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
39pub fn push_once(session: &Session, remote: Option<&str>, now: i64) -> Result<PushOutput> {
64 let repo = &session.repo;
65 let ns = session.namespace();
66
67 let remote_name = git_utils::resolve_meta_remote(repo, remote)?;
68 let local_ref = session.local_ref();
69 let remote_refspec = format!("refs/{ns}/main");
70
71 let _ = crate::serialize::run(session, now)?;
73
74 if repo.find_reference(&local_ref).is_err() {
76 return Err(Error::Other(
77 "nothing to push (no local metadata ref)".into(),
78 ));
79 }
80
81 let remote_tracking_ref = format!("refs/{ns}/remotes/main");
83 let local_oid = repo
84 .find_reference(&local_ref)
85 .ok()
86 .and_then(|r| r.into_fully_peeled_id().ok())
87 .map(gix::Id::detach);
88 let remote_oid = repo
89 .find_reference(&remote_tracking_ref)
90 .ok()
91 .and_then(|r| r.into_fully_peeled_id().ok())
92 .map(gix::Id::detach);
93
94 if let (Some(local), Some(remote_id)) = (local_oid.as_ref(), remote_oid.as_ref()) {
95 if local == remote_id {
96 return Ok(PushOutput {
97 success: true,
98 non_fast_forward: false,
99 up_to_date: true,
100 remote_name,
101 remote_ref: remote_refspec,
102 commit_oid: local.to_string(),
103 });
104 }
105 }
106
107 let commit_oid_str = local_oid
108 .as_ref()
109 .map(ToString::to_string)
110 .unwrap_or_default();
111
112 let push_refspec = format!("{local_ref}:{remote_refspec}");
114 let result = git_utils::run_git(repo, &["push", &remote_name, &push_refspec]);
115
116 match result {
117 Ok(_) => Ok(PushOutput {
118 success: true,
119 non_fast_forward: false,
120 up_to_date: false,
121 remote_name,
122 remote_ref: remote_refspec,
123 commit_oid: commit_oid_str,
124 }),
125 Err(e) => {
126 let err_msg = e.to_string();
127 let is_non_ff = err_msg.contains("non-fast-forward")
128 || err_msg.contains("rejected")
129 || err_msg.contains("fetch first");
130
131 if is_non_ff {
132 Ok(PushOutput {
133 success: false,
134 non_fast_forward: true,
135 up_to_date: false,
136 remote_name,
137 remote_ref: remote_refspec,
138 commit_oid: commit_oid_str,
139 })
140 } else {
141 Err(Error::GitCommand(format!("push failed: {err_msg}")))
142 }
143 }
144 }
145}
146
147pub fn resolve_push_conflict(session: &Session, remote: Option<&str>, now: i64) -> Result<()> {
167 let repo = &session.repo;
168 let ns = session.namespace();
169
170 let remote_name = git_utils::resolve_meta_remote(repo, remote)?;
171 let local_ref = session.local_ref();
172 let remote_refspec = format!("refs/{ns}/main");
173 let remote_tracking_ref = format!("refs/{ns}/remotes/main");
174
175 let fetch_refspec = format!("{remote_refspec}:{remote_tracking_ref}");
177 git_utils::run_git(repo, &["fetch", &remote_name, &fetch_refspec])?;
178
179 let short_ref = format!("{ns}/remotes/main");
181 git_utils::hydrate_tip_blobs(repo, &remote_name, &short_ref)?;
182
183 let _ = crate::materialize::run(session, None, now)?;
185
186 let _ = crate::serialize::run(session, now)?;
188
189 rebase_local_on_remote(repo, &local_ref, &remote_tracking_ref)?;
193
194 Ok(())
195}
196
197fn rebase_local_on_remote(repo: &gix::Repository, local_ref: &str, remote_ref: &str) -> Result<()> {
203 let local_ref_obj = repo
204 .find_reference(local_ref)
205 .map_err(|e| Error::Other(format!("{e}")))?;
206 let local_oid = local_ref_obj
207 .into_fully_peeled_id()
208 .map_err(|e| Error::Other(format!("{e}")))?
209 .detach();
210 let local_commit_obj = local_oid
211 .attach(repo)
212 .object()
213 .map_err(|e| Error::Other(format!("{e}")))?
214 .into_commit();
215 let local_decoded = local_commit_obj
216 .decode()
217 .map_err(|e| Error::Other(format!("{e}")))?;
218
219 let remote_ref_obj = repo
220 .find_reference(remote_ref)
221 .map_err(|e| Error::Other(format!("{e}")))?;
222 let remote_oid = remote_ref_obj
223 .into_fully_peeled_id()
224 .map_err(|e| Error::Other(format!("{e}")))?
225 .detach();
226
227 let parent_ids: Vec<gix::ObjectId> = local_decoded.parents().collect();
229 if parent_ids.len() == 1 && parent_ids[0] == remote_oid {
230 return Ok(());
231 }
232
233 let tree_id = local_decoded.tree();
234 let message = local_decoded.message.to_owned();
235 let author_ref = local_decoded
236 .author()
237 .map_err(|e| Error::Other(format!("{e}")))?;
238
239 let commit = gix::objs::Commit {
240 message,
241 tree: tree_id,
242 author: gix::actor::Signature {
243 name: author_ref.name.into(),
244 email: author_ref.email.into(),
245 time: author_ref
246 .time()
247 .map_err(|e| Error::Other(format!("{e}")))?,
248 },
249 committer: gix::actor::Signature {
250 name: author_ref.name.into(),
251 email: author_ref.email.into(),
252 time: author_ref
253 .time()
254 .map_err(|e| Error::Other(format!("{e}")))?,
255 },
256 encoding: None,
257 parents: vec![remote_oid].into(),
258 extra_headers: Default::default(),
259 };
260
261 let new_oid = repo
262 .write_object(&commit)
263 .map_err(|e| Error::Other(format!("{e}")))?
264 .detach();
265 repo.reference(
266 local_ref,
267 new_oid,
268 PreviousValue::Any,
269 "git-meta: rebase for push",
270 )
271 .map_err(|e| Error::Other(format!("{e}")))?;
272
273 Ok(())
274}