1use git2::{Cred, Error, FetchOptions, RemoteCallbacks, Repository};
2use serde::{Deserialize, Serialize};
3
4use crate::{core::watcher::WatchContext, git::remote::find_ssh_key};
5
6#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
7pub struct Repo {
8 pub branches: Branches,
9 pub name: String,
10 pub remote: String,
11}
12
13#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
14pub struct Branch {
15 pub branch: String, pub last_commit: String, pub remote: String, pub name: String, }
21
22#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
23pub struct Branches {
24 pub branches: Vec<Branch>,
25 pub last_commit: String, pub last_name: String, pub name: String, }
29
30impl Branches {
31 pub fn last_mut(&mut self) -> anyhow::Result<&mut Branch> {
32 if let Some(last) = self.branches.last_mut() {
33 Ok(last)
34 } else {
35 anyhow::bail!("failed to recover branch");
36 }
37 }
38
39 pub fn last(&self) -> anyhow::Result<&Branch> {
40 if let Some(last) = self.branches.last() {
41 Ok(last)
42 } else {
43 anyhow::bail!("failed to recover branch");
44 }
45 }
46
47 pub fn default_last_commit(&self) -> anyhow::Result<String> {
48 let last = self.last()?;
49 Ok(last.last_commit.clone())
50 }
51
52 pub fn try_for_each<F, T>(&mut self, mut f: F) -> anyhow::Result<Vec<T>>
53 where
54 F: FnMut(&mut Branch) -> anyhow::Result<T>,
55 {
56 let mut results = Vec::new();
57
58 for branch in &mut self.branches {
59 results.push(f(branch)?);
60 }
61
62 Ok(results)
63 }
64}
65
66impl From<Vec<Branch>> for Branches {
67 fn from(branches: Vec<Branch>) -> Self {
68 Branches {
69 branches,
70 last_commit: String::default(),
71 name: String::default(),
72 last_name: String::default(),
73 }
74 }
75}
76
77impl From<Branch> for Branches {
78 fn from(branch: Branch) -> Self {
79 Branches {
80 branches: vec![branch],
81 last_commit: String::default(),
82 name: String::default(),
83 last_name: String::default(),
84 }
85 }
86}
87
88impl Repo {
89 pub fn build(branch_names: Vec<String>) -> anyhow::Result<Self> {
90 let repo = Repository::open(".")?;
91 let mut remote = String::new();
92 let mut repo_name = String::new();
93
94 let branches: Vec<Branch> = branch_names
95 .iter()
96 .map(|name| {
97 let (branch, commit) = {
98 let branch_ref = repo.find_branch(name, git2::BranchType::Remote)?;
99 let target = branch_ref.get().peel_to_commit()?;
100
101 let branch_name = branch_ref
102 .name()?
103 .ok_or_else(|| Error::from_str("Failed to read branch name"))?
104 .to_string();
105
106 (branch_name, target.id().to_string())
107 };
108 remote = repo
109 .find_remote("origin")?
110 .url()
111 .ok_or_else(|| Error::from_str("Remote URL 'origin' not found"))?
112 .to_string();
113 repo_name = remote
114 .rsplit('/')
115 .next()
116 .and_then(|s| s.strip_suffix(".git").or(Some(s)))
117 .ok_or_else(|| Error::from_str("Failed to parse repo name from remote URL"))?
118 .to_string();
119 Ok(Branch {
120 branch,
121 last_commit: commit,
122 remote: remote.clone(),
123 name: repo_name.clone(),
124 })
125 })
126 .collect::<anyhow::Result<Vec<_>>>()?;
127
128 Ok(Self {
129 branches: branches.into(),
130 remote,
131 name: repo_name,
132 })
133 }
134
135 pub fn default_build() -> anyhow::Result<Self> {
136 let repo = Repository::open(".")?;
137
138 let branch = {
139 let head = repo.head()?;
140 let branch = head
141 .shorthand()
142 .ok_or_else(|| Error::from_str("Failed to read branch name"))?
143 .to_string();
144 let commit_id = head.peel_to_commit()?.id().to_string();
145
146 let remote = repo
147 .find_remote("origin")?
148 .url()
149 .ok_or_else(|| Error::from_str("Remote URL 'origin' not found"))?
150 .to_string();
151
152 let repo_name = remote
153 .rsplit('/')
154 .next()
155 .and_then(|s| s.strip_suffix(".git").or(Some(s)))
156 .ok_or_else(|| Error::from_str("Failed to parse repo name from remote URL"))?
157 .to_string();
158
159 Branch {
160 branch,
161 last_commit: commit_id,
162 remote: remote.clone(),
163 name: repo_name.clone(),
164 }
165 };
166
167 let remote = repo
170 .find_remote("origin")?
171 .url()
172 .ok_or_else(|| Error::from_str("Remote URL 'origin' not found"))?
173 .to_string();
174
175 let repo_name = remote
176 .rsplit('/')
177 .next()
178 .and_then(|s| s.strip_suffix(".git").or(Some(s)))
179 .ok_or_else(|| Error::from_str("Failed to parse repo name from remote URL"))?
180 .to_string();
181
182 Ok(Self {
183 branches: branch.into(),
184 remote,
185 name: repo_name,
186 })
187 }
188
189 pub fn pull(repo: &Repository, branch_name: &str) -> anyhow::Result<()> {
190 let ssh_key_path = find_ssh_key()?;
191
192 let mut cb = RemoteCallbacks::new();
193
194 cb.credentials(|_, username_from_url, _| {
195 Cred::ssh_key(username_from_url.unwrap(), None, &ssh_key_path, None)
196 });
197
198 let mut fo = FetchOptions::new();
199 fo.remote_callbacks(cb);
200
201 let mut remote = repo.find_remote("origin")?;
202 remote.fetch(&[branch_name], Some(&mut fo), None)?;
203 Ok(())
204 }
205
206 pub fn switch_branch(ctx: &WatchContext, remote_branch: &str) -> anyhow::Result<()> {
207 Repo::switch_branch_inner(ctx, remote_branch, 0)
208 }
209
210 fn switch_branch_inner(
217 ctx: &WatchContext,
218 remote_branch: &str,
219 attempt: u8,
220 ) -> anyhow::Result<()> {
221 const MAX_ATTEMPTS: u8 = 2;
222
223 if attempt > MAX_ATTEMPTS {
224 anyhow::bail!(
225 "Unable to find branch `{}` locally or remotely",
226 remote_branch
227 );
228 }
229
230 let repo = Repository::open(&ctx.project_dir)?;
231
232 let branch_name = remote_branch
233 .strip_prefix("origin/")
234 .unwrap_or(remote_branch);
235
236 if repo.head()?.shorthand().unwrap_or_default() == branch_name {
237 eprintln!("already on the good branch");
239 return Ok(());
240 }
241
242 let branch = repo.find_branch(branch_name, git2::BranchType::Local);
243 match branch {
244 Ok(b) => {
245 let branch_ref = b.get();
246 let commit = branch_ref.peel_to_commit()?;
247 repo.set_head(branch_ref.name().unwrap())?;
248 repo.checkout_tree(commit.as_object(), None)?;
249 Ok(())
250 }
251 Err(_) => {
252 match repo.find_branch(remote_branch, git2::BranchType::Remote) {
253 Ok(remote_ref) => {
254 let target_commit = remote_ref.get().peel_to_commit()?;
256 repo.branch(branch_name, &target_commit, false)?;
257 Repo::switch_branch_inner(ctx, remote_branch, attempt + 1)
258 }
259 Err(_) => {
260 Repo::pull(&repo, branch_name)?;
262 Repo::switch_branch_inner(ctx, remote_branch, attempt + 1)
263 }
264 }
265 }
266 }
267 }
268}