1use std::collections::{BTreeMap, HashSet};
16use std::default::Default;
17use std::path::PathBuf;
18
19use git2::Oid;
20use itertools::Itertools;
21use thiserror::Error;
22
23use crate::backend::{CommitId, ObjectId};
24use crate::commit::Commit;
25use crate::git_backend::NO_GC_REF_NAMESPACE;
26use crate::op_store::RefTarget;
27use crate::repo::{MutableRepo, Repo};
28use crate::settings::GitSettings;
29use crate::view::RefName;
30
31#[derive(Error, Debug, PartialEq)]
32pub enum GitImportError {
33 #[error("Unexpected git error when importing refs: {0}")]
34 InternalGitError(#[from] git2::Error),
35}
36
37fn parse_git_ref(ref_name: &str) -> Option<RefName> {
38 if let Some(branch_name) = ref_name.strip_prefix("refs/heads/") {
39 Some(RefName::LocalBranch(branch_name.to_string()))
40 } else if let Some(remote_and_branch) = ref_name.strip_prefix("refs/remotes/") {
41 remote_and_branch
42 .split_once('/')
43 .map(|(remote, branch)| RefName::RemoteBranch {
44 remote: remote.to_string(),
45 branch: branch.to_string(),
46 })
47 } else {
48 ref_name
49 .strip_prefix("refs/tags/")
50 .map(|tag_name| RefName::Tag(tag_name.to_string()))
51 }
52}
53
54fn prevent_gc(git_repo: &git2::Repository, id: &CommitId) {
55 git_repo
56 .reference(
57 &format!("{}{}", NO_GC_REF_NAMESPACE, id.hex()),
58 Oid::from_bytes(id.as_bytes()).unwrap(),
59 true,
60 "used by jj",
61 )
62 .unwrap();
63}
64
65pub fn import_refs(
67 mut_repo: &mut MutableRepo,
68 git_repo: &git2::Repository,
69 git_settings: &GitSettings,
70) -> Result<(), GitImportError> {
71 let store = mut_repo.store().clone();
72 let mut existing_git_refs = mut_repo.view().git_refs().clone();
73 let mut old_git_heads = existing_git_refs
74 .values()
75 .flat_map(|old_target| old_target.adds())
76 .collect_vec();
77 if let Some(old_git_head) = mut_repo.view().git_head() {
78 old_git_heads.extend(old_git_head.adds());
79 }
80
81 let mut new_git_heads = HashSet::new();
82 if let Ok(head_git_commit) = git_repo
85 .head()
86 .and_then(|head_ref| head_ref.peel_to_commit())
87 {
88 let head_commit_id = CommitId::from_bytes(head_git_commit.id().as_bytes());
89 let head_commit = store.get_commit(&head_commit_id).unwrap();
90 new_git_heads.insert(head_commit_id.clone());
91 prevent_gc(git_repo, &head_commit_id);
92 mut_repo.add_head(&head_commit);
93 mut_repo.set_git_head(RefTarget::Normal(head_commit_id));
94 } else {
95 mut_repo.clear_git_head();
96 }
97
98 let mut changed_git_refs = BTreeMap::new();
99 let git_refs = git_repo.references()?;
100 for git_ref in git_refs {
101 let git_ref = git_ref?;
102 if !(git_ref.is_tag() || git_ref.is_branch() || git_ref.is_remote())
103 || git_ref.name().is_none()
104 {
105 continue;
107 }
108 let full_name = git_ref.name().unwrap().to_string();
109 if let Some(RefName::RemoteBranch { branch, remote: _ }) = parse_git_ref(&full_name) {
110 if &branch == "HEAD" {
112 continue;
113 }
114 }
115 let git_commit = match git_ref.peel_to_commit() {
116 Ok(git_commit) => git_commit,
117 Err(_) => {
118 continue;
120 }
121 };
122 let id = CommitId::from_bytes(git_commit.id().as_bytes());
123 new_git_heads.insert(id.clone());
124 let old_target = existing_git_refs.remove(&full_name);
127 let new_target = Some(RefTarget::Normal(id.clone()));
128 if new_target != old_target {
129 prevent_gc(git_repo, &id);
130 mut_repo.set_git_ref(full_name.clone(), RefTarget::Normal(id.clone()));
131 let commit = store.get_commit(&id).unwrap();
132 mut_repo.add_head(&commit);
133 changed_git_refs.insert(full_name, (old_target, new_target));
134 }
135 }
136 for (full_name, target) in existing_git_refs {
137 mut_repo.remove_git_ref(&full_name);
138 changed_git_refs.insert(full_name, (Some(target), None));
139 }
140 for (full_name, (old_git_target, new_git_target)) in changed_git_refs {
141 if let Some(ref_name) = parse_git_ref(&full_name) {
142 mut_repo.merge_single_ref(&ref_name, old_git_target.as_ref(), new_git_target.as_ref());
144 if !git_settings.auto_local_branch {
147 continue;
148 }
149 if let RefName::RemoteBranch { branch, remote: _ } = ref_name {
150 mut_repo.merge_single_ref(
151 &RefName::LocalBranch(branch),
152 old_git_target.as_ref(),
153 new_git_target.as_ref(),
154 );
155 }
156 }
157 }
158
159 let new_git_heads = new_git_heads.into_iter().collect_vec();
162 let abandoned_commits = mut_repo
167 .index()
168 .walk_revs(&old_git_heads, &new_git_heads)
169 .map(|entry| entry.commit_id())
170 .collect_vec();
171 let root_commit_id = mut_repo.store().root_commit_id().clone();
172 for abandoned_commit in abandoned_commits {
173 if abandoned_commit != root_commit_id {
174 mut_repo.record_abandoned_commit(abandoned_commit);
175 }
176 }
177
178 Ok(())
179}
180
181#[derive(Error, Debug, PartialEq)]
182pub enum GitExportError {
183 #[error("Cannot export conflicted branch '{0}'")]
184 ConflictedBranch(String),
185 #[error("Failed to read export state: {0}")]
186 ReadStateError(String),
187 #[error("Failed to write export state: {0}")]
188 WriteStateError(String),
189 #[error("Git error: {0}")]
190 InternalGitError(#[from] git2::Error),
191}
192
193pub fn export_refs(
198 mut_repo: &mut MutableRepo,
199 git_repo: &git2::Repository,
200) -> Result<Vec<String>, GitExportError> {
201 let mut branches_to_update = BTreeMap::new();
203 let mut branches_to_delete = BTreeMap::new();
204 let mut failed_branches = vec![];
205 let view = mut_repo.view();
206 let all_branch_names: HashSet<&str> = view
207 .git_refs()
208 .keys()
209 .filter_map(|git_ref| git_ref.strip_prefix("refs/heads/"))
210 .chain(view.branches().keys().map(AsRef::as_ref))
211 .collect();
212 for branch_name in all_branch_names {
213 let old_branch = view.get_git_ref(&format!("refs/heads/{branch_name}"));
214 let new_branch = view.get_local_branch(branch_name);
215 if new_branch == old_branch {
216 continue;
217 }
218 let old_oid = match old_branch {
219 None => None,
220 Some(RefTarget::Normal(id)) => Some(Oid::from_bytes(id.as_bytes()).unwrap()),
221 Some(RefTarget::Conflict { .. }) => {
222 failed_branches.push(branch_name.to_owned());
225 continue;
226 }
227 };
228 if let Some(new_branch) = new_branch {
229 match new_branch {
230 RefTarget::Normal(id) => {
231 let new_oid = Oid::from_bytes(id.as_bytes());
232 branches_to_update.insert(branch_name.to_owned(), (old_oid, new_oid.unwrap()));
233 }
234 RefTarget::Conflict { .. } => {
235 continue;
237 }
238 }
239 } else {
240 branches_to_delete.insert(branch_name.to_owned(), old_oid.unwrap());
241 }
242 }
243 if let Ok(head_ref) = git_repo.find_reference("HEAD") {
245 if let (Some(head_git_ref), Ok(current_git_commit)) =
246 (head_ref.symbolic_target(), head_ref.peel_to_commit())
247 {
248 if let Some(branch_name) = head_git_ref.strip_prefix("refs/heads/") {
249 let detach_head =
250 if let Some((_old_oid, new_oid)) = branches_to_update.get(branch_name) {
251 *new_oid != current_git_commit.id()
252 } else {
253 branches_to_delete.contains_key(branch_name)
254 };
255 if detach_head {
256 git_repo.set_head_detached(current_git_commit.id())?;
257 }
258 }
259 }
260 }
261 for (branch_name, old_oid) in branches_to_delete {
262 let git_ref_name = format!("refs/heads/{branch_name}");
263 let success = if let Ok(mut git_ref) = git_repo.find_reference(&git_ref_name) {
264 if git_ref.target() == Some(old_oid) {
265 git_ref.delete().is_ok()
267 } else {
268 false
270 }
271 } else {
272 true
274 };
275 if success {
276 mut_repo.remove_git_ref(&git_ref_name);
277 } else {
278 failed_branches.push(branch_name);
279 }
280 }
281 for (branch_name, (old_oid, new_oid)) in branches_to_update {
282 let git_ref_name = format!("refs/heads/{branch_name}");
283 let success = match old_oid {
284 None => {
285 if let Ok(git_ref) = git_repo.find_reference(&git_ref_name) {
286 git_ref.target() == Some(new_oid)
289 } else {
290 git_repo
292 .reference(&git_ref_name, new_oid, true, "export from jj")
293 .is_ok()
294 }
295 }
296 Some(old_oid) => {
297 if git_repo
300 .reference_matching(&git_ref_name, new_oid, true, old_oid, "export from jj")
301 .is_ok()
302 {
303 true
305 } else {
306 if let Ok(git_ref) = git_repo.find_reference(&git_ref_name) {
308 git_ref.target() == Some(new_oid)
310 } else {
311 false
313 }
314 }
315 }
316 };
317 if success {
318 mut_repo.set_git_ref(
319 git_ref_name,
320 RefTarget::Normal(CommitId::from_bytes(new_oid.as_bytes())),
321 );
322 } else {
323 failed_branches.push(branch_name);
324 }
325 }
326 Ok(failed_branches)
327}
328
329#[derive(Error, Debug, PartialEq)]
330pub enum GitFetchError {
331 #[error("No git remote named '{0}'")]
332 NoSuchRemote(String),
333 #[error("Unexpected git error when fetching: {0}")]
335 InternalGitError(#[from] git2::Error),
336}
337
338#[tracing::instrument(skip(mut_repo, git_repo, callbacks))]
339pub fn fetch(
340 mut_repo: &mut MutableRepo,
341 git_repo: &git2::Repository,
342 remote_name: &str,
343 callbacks: RemoteCallbacks<'_>,
344 git_settings: &GitSettings,
345) -> Result<Option<String>, GitFetchError> {
346 let mut remote =
347 git_repo
348 .find_remote(remote_name)
349 .map_err(|err| match (err.class(), err.code()) {
350 (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
351 GitFetchError::NoSuchRemote(remote_name.to_string())
352 }
353 (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
354 GitFetchError::NoSuchRemote(remote_name.to_string())
355 }
356 _ => GitFetchError::InternalGitError(err),
357 })?;
358 let mut fetch_options = git2::FetchOptions::new();
359 let mut proxy_options = git2::ProxyOptions::new();
360 proxy_options.auto();
361 fetch_options.proxy_options(proxy_options);
362 let callbacks = callbacks.into_git();
363 fetch_options.remote_callbacks(callbacks);
364 let refspec: &[&str] = &[];
365 tracing::debug!("remote.download");
366 remote.download(refspec, Some(&mut fetch_options))?;
367 tracing::debug!("remote.prune");
368 remote.prune(None)?;
369 tracing::debug!("remote.update_tips");
370 remote.update_tips(None, false, git2::AutotagOption::Unspecified, None)?;
371 let mut default_branch = None;
374 if let Ok(default_ref_buf) = remote.default_branch() {
375 if let Some(default_ref) = default_ref_buf.as_str() {
376 if let Some(RefName::LocalBranch(branch_name)) = parse_git_ref(default_ref) {
379 tracing::debug!(default_branch = branch_name);
380 default_branch = Some(branch_name);
381 }
382 }
383 }
384 tracing::debug!("remote.disconnect");
385 remote.disconnect()?;
386 tracing::debug!("import_refs");
387 import_refs(mut_repo, git_repo, git_settings).map_err(|err| match err {
388 GitImportError::InternalGitError(source) => GitFetchError::InternalGitError(source),
389 })?;
390 Ok(default_branch)
391}
392
393#[derive(Error, Debug, PartialEq)]
394pub enum GitPushError {
395 #[error("No git remote named '{0}'")]
396 NoSuchRemote(String),
397 #[error("Push is not fast-forwardable")]
398 NotFastForward,
399 #[error("Remote rejected the update of some refs (do you have permission to push to {0:?}?)")]
400 RefUpdateRejected(Vec<String>),
401 #[error("Unexpected git error when pushing: {0}")]
404 InternalGitError(#[from] git2::Error),
405}
406
407pub fn push_commit(
408 git_repo: &git2::Repository,
409 target: &Commit,
410 remote_name: &str,
411 remote_branch: &str,
412 force: bool,
416 callbacks: RemoteCallbacks<'_>,
417) -> Result<(), GitPushError> {
418 push_updates(
419 git_repo,
420 remote_name,
421 &[GitRefUpdate {
422 qualified_name: format!("refs/heads/{remote_branch}"),
423 force,
424 new_target: Some(target.id().clone()),
425 }],
426 callbacks,
427 )
428}
429
430pub struct GitRefUpdate {
431 pub qualified_name: String,
432 pub force: bool,
436 pub new_target: Option<CommitId>,
437}
438
439pub fn push_updates(
440 git_repo: &git2::Repository,
441 remote_name: &str,
442 updates: &[GitRefUpdate],
443 callbacks: RemoteCallbacks<'_>,
444) -> Result<(), GitPushError> {
445 let mut temp_refs = vec![];
446 let mut qualified_remote_refs = vec![];
447 let mut refspecs = vec![];
448 for update in updates {
449 qualified_remote_refs.push(update.qualified_name.as_str());
450 if let Some(new_target) = &update.new_target {
451 let temp_ref_name = format!("refs/jj/git-push/{}", new_target.hex());
453 temp_refs.push(git_repo.reference(
454 &temp_ref_name,
455 git2::Oid::from_bytes(new_target.as_bytes()).unwrap(),
456 true,
457 "temporary reference for git push",
458 )?);
459 refspecs.push(format!(
460 "{}{}:{}",
461 (if update.force { "+" } else { "" }),
462 temp_ref_name,
463 update.qualified_name
464 ));
465 } else {
466 refspecs.push(format!(":{}", update.qualified_name));
467 }
468 }
469 let result = push_refs(
470 git_repo,
471 remote_name,
472 &qualified_remote_refs,
473 &refspecs,
474 callbacks,
475 );
476 for mut temp_ref in temp_refs {
477 if let Err(err) = temp_ref.delete() {
480 if result.is_ok() && err.code() != git2::ErrorCode::NotFound {
484 return Err(GitPushError::InternalGitError(err));
485 }
486 }
487 }
488 result
489}
490
491fn push_refs(
492 git_repo: &git2::Repository,
493 remote_name: &str,
494 qualified_remote_refs: &[&str],
495 refspecs: &[String],
496 callbacks: RemoteCallbacks<'_>,
497) -> Result<(), GitPushError> {
498 let mut remote =
499 git_repo
500 .find_remote(remote_name)
501 .map_err(|err| match (err.class(), err.code()) {
502 (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
503 GitPushError::NoSuchRemote(remote_name.to_string())
504 }
505 (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
506 GitPushError::NoSuchRemote(remote_name.to_string())
507 }
508 _ => GitPushError::InternalGitError(err),
509 })?;
510 let mut remaining_remote_refs: HashSet<_> = qualified_remote_refs.iter().copied().collect();
511 let mut push_options = git2::PushOptions::new();
512 let mut proxy_options = git2::ProxyOptions::new();
513 proxy_options.auto();
514 push_options.proxy_options(proxy_options);
515 let mut callbacks = callbacks.into_git();
516 callbacks.push_update_reference(|refname, status| {
517 if status.is_none() {
519 remaining_remote_refs.remove(refname);
520 }
521 Ok(())
522 });
523 push_options.remote_callbacks(callbacks);
524 remote
525 .push(refspecs, Some(&mut push_options))
526 .map_err(|err| match (err.class(), err.code()) {
527 (git2::ErrorClass::Reference, git2::ErrorCode::NotFastForward) => {
528 GitPushError::NotFastForward
529 }
530 _ => GitPushError::InternalGitError(err),
531 })?;
532 drop(push_options);
533 if remaining_remote_refs.is_empty() {
534 Ok(())
535 } else {
536 Err(GitPushError::RefUpdateRejected(
537 remaining_remote_refs
538 .iter()
539 .sorted()
540 .map(|name| name.to_string())
541 .collect(),
542 ))
543 }
544}
545
546#[non_exhaustive]
547#[derive(Default)]
548#[allow(clippy::type_complexity)]
549pub struct RemoteCallbacks<'a> {
550 pub progress: Option<&'a mut dyn FnMut(&Progress)>,
551 pub get_ssh_key: Option<&'a mut dyn FnMut(&str) -> Option<PathBuf>>,
552 pub get_password: Option<&'a mut dyn FnMut(&str, &str) -> Option<String>>,
553 pub get_username_password: Option<&'a mut dyn FnMut(&str) -> Option<(String, String)>>,
554}
555
556impl<'a> RemoteCallbacks<'a> {
557 fn into_git(mut self) -> git2::RemoteCallbacks<'a> {
558 let mut callbacks = git2::RemoteCallbacks::new();
559 if let Some(progress_cb) = self.progress {
560 callbacks.transfer_progress(move |progress| {
561 progress_cb(&Progress {
562 bytes_downloaded: (progress.received_objects() < progress.total_objects())
563 .then(|| progress.received_bytes() as u64),
564 overall: (progress.indexed_objects() + progress.indexed_deltas()) as f32
565 / (progress.total_objects() + progress.total_deltas()) as f32,
566 });
567 true
568 });
569 }
570 callbacks.credentials(move |url, username_from_url, allowed_types| {
573 let span = tracing::debug_span!("RemoteCallbacks.credentials");
574 let _ = span.enter();
575
576 let git_config = git2::Config::open_default();
577 let credential_helper = git_config
578 .and_then(|conf| git2::Cred::credential_helper(&conf, url, username_from_url));
579 if let Ok(creds) = credential_helper {
580 tracing::debug!("using credential_helper");
581 return Ok(creds);
582 } else if let Some(username) = username_from_url {
583 if allowed_types.contains(git2::CredentialType::SSH_KEY) {
584 if std::env::var("SSH_AUTH_SOCK").is_ok()
585 || std::env::var("SSH_AGENT_PID").is_ok()
586 {
587 tracing::debug!(username, "using ssh_key_from_agent");
588 return git2::Cred::ssh_key_from_agent(username).map_err(|err| {
589 tracing::error!(err = %err);
590 err
591 });
592 }
593 if let Some(ref mut cb) = self.get_ssh_key {
594 if let Some(path) = cb(username) {
595 tracing::debug!(username, path = ?path, "using ssh_key");
596 return git2::Cred::ssh_key(username, None, &path, None).map_err(
597 |err| {
598 tracing::error!(err = %err);
599 err
600 },
601 );
602 }
603 }
604 }
605 if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
606 if let Some(ref mut cb) = self.get_password {
607 if let Some(pw) = cb(url, username) {
608 tracing::debug!(
609 username,
610 "using userpass_plaintext with username from url"
611 );
612 return git2::Cred::userpass_plaintext(username, &pw).map_err(|err| {
613 tracing::error!(err = %err);
614 err
615 });
616 }
617 }
618 }
619 } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
620 if let Some(ref mut cb) = self.get_username_password {
621 if let Some((username, pw)) = cb(url) {
622 tracing::debug!(username, "using userpass_plaintext");
623 return git2::Cred::userpass_plaintext(&username, &pw).map_err(|err| {
624 tracing::error!(err = %err);
625 err
626 });
627 }
628 }
629 }
630 tracing::debug!("using default");
631 git2::Cred::default()
632 });
633 callbacks
634 }
635}
636
637pub struct Progress {
638 pub bytes_downloaded: Option<u64>,
640 pub overall: f32,
641}