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