1use crate::commit::Commit;
16use crate::store::CommitId;
17use crate::transaction::Transaction;
18use thiserror::Error;
19
20#[derive(Error, Debug, PartialEq)]
21pub enum GitImportError {
22 #[error("The repo is not backed by a git repo")]
23 NotAGitRepo,
24 #[error("Unexpected git error when importing refs: {0}")]
25 InternalGitError(#[from] git2::Error),
26}
27
28pub fn import_refs(tx: &mut Transaction) -> Result<(), GitImportError> {
30 let store = tx.store().clone();
31 let git_repo = store.git_repo().ok_or(GitImportError::NotAGitRepo)?;
32 let git_refs = git_repo.references()?;
33 for git_ref in git_refs {
34 let git_ref = git_ref?;
35 if !(git_ref.is_tag() || git_ref.is_branch() || git_ref.is_remote()) {
36 continue;
39 }
40 let git_commit = git_ref.peel_to_commit()?;
41 let id = CommitId(git_commit.id().as_bytes().to_vec());
42 let commit = store.get_commit(&id).unwrap();
43 tx.add_head(&commit);
44 }
45 Ok(())
46}
47
48#[derive(Error, Debug, PartialEq)]
49pub enum GitFetchError {
50 #[error("The repo is not backed by a git repo")]
51 NotAGitRepo,
52 #[error("No git remote named '{0}'")]
53 NoSuchRemote(String),
54 #[error("Unexpected git error when fetching: {0}")]
56 InternalGitError(#[from] git2::Error),
57}
58
59pub fn fetch(tx: &mut Transaction, remote_name: &str) -> Result<(), GitFetchError> {
60 let git_repo = tx.store().git_repo().ok_or(GitFetchError::NotAGitRepo)?;
61 let mut remote =
62 git_repo
63 .find_remote(remote_name)
64 .map_err(|err| match (err.class(), err.code()) {
65 (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
66 GitFetchError::NoSuchRemote(remote_name.to_string())
67 }
68 (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
69 GitFetchError::NoSuchRemote(remote_name.to_string())
70 }
71 _ => GitFetchError::InternalGitError(err),
72 })?;
73 let refspec: &[&str] = &[];
74 remote.fetch(refspec, None, None)?;
75 import_refs(tx).map_err(|err| match err {
76 GitImportError::NotAGitRepo => panic!("git repo somehow became a non-git repo"),
77 GitImportError::InternalGitError(source) => GitFetchError::InternalGitError(source),
78 })?;
79 Ok(())
80}
81
82#[derive(Error, Debug, PartialEq)]
83pub enum GitPushError {
84 #[error("The repo is not backed by a git repo")]
85 NotAGitRepo,
86 #[error("No git remote named '{0}'")]
87 NoSuchRemote(String),
88 #[error("Push is not fast-forwardable'")]
89 NotFastForward,
90 #[error("Remote reject the update'")]
91 RefUpdateRejected,
92 #[error("Unexpected git error when pushing: {0}")]
95 InternalGitError(#[from] git2::Error),
96}
97
98pub fn push_commit(
99 commit: &Commit,
100 remote_name: &str,
101 remote_branch: &str,
102) -> Result<(), GitPushError> {
103 let git_repo = commit.store().git_repo().ok_or(GitPushError::NotAGitRepo)?;
104 let temp_ref_name = format!("refs/jj/git-push/{}", commit.id().hex());
106 let mut temp_ref = git_repo.reference(
107 &temp_ref_name,
108 git2::Oid::from_bytes(&commit.id().0).unwrap(),
109 true,
110 "temporary reference for git push",
111 )?;
112 let mut remote =
113 git_repo
114 .find_remote(remote_name)
115 .map_err(|err| match (err.class(), err.code()) {
116 (git2::ErrorClass::Config, git2::ErrorCode::NotFound) => {
117 GitPushError::NoSuchRemote(remote_name.to_string())
118 }
119 (git2::ErrorClass::Config, git2::ErrorCode::InvalidSpec) => {
120 GitPushError::NoSuchRemote(remote_name.to_string())
121 }
122 _ => GitPushError::InternalGitError(err),
123 })?;
124 let qualified_remote_branch = format!("refs/heads/{}", remote_branch);
126 let mut callbacks = git2::RemoteCallbacks::new();
127 let mut updated = false;
128 callbacks.credentials(|_url, username_from_url, _allowed_types| {
129 git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
130 });
131 callbacks.push_update_reference(|refname, status| {
132 if refname == qualified_remote_branch && status.is_none() {
133 updated = true;
134 }
135 Ok(())
136 });
137 let refspec = format!("{}:{}", temp_ref_name, qualified_remote_branch);
138 let mut push_options = git2::PushOptions::new();
139 push_options.remote_callbacks(callbacks);
140 remote
141 .push(&[refspec], Some(&mut push_options))
142 .map_err(|err| match (err.class(), err.code()) {
143 (git2::ErrorClass::Reference, git2::ErrorCode::NotFastForward) => {
144 GitPushError::NotFastForward
145 }
146 _ => GitPushError::InternalGitError(err),
147 })?;
148 drop(push_options);
149 temp_ref.delete()?;
150 if updated {
151 Ok(())
152 } else {
153 Err(GitPushError::RefUpdateRejected)
154 }
155}