1use std::{fmt, path};
2
3use anyhow::*;
4use std::result::Result::Ok;
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum MergeResult {
8 UpToDate,
9 FastForward,
10 Merged,
11 Conflicts,
12}
13
14pub struct Repo {
15 pub git_repo: git2::Repository,
16 pub work_dir: path::PathBuf,
17 pub head: String,
18 pub subrepos: Vec<Repo>,
19}
20
21impl Repo {
22 pub fn new(work_dir: &path::Path, head_name: Option<&str>) -> Result<Self> {
23 println!("Reading repo at `{}`", work_dir.display());
24
25 let git_repo = git2::Repository::open(work_dir)
26 .with_context(|| format!("Cannot open repo at `{}`", work_dir.display()))?;
27
28 let head = match head_name {
29 Some(name) => String::from(name),
30 None => {
31 if git_repo.head_detached().with_context(|| {
32 format!(
33 "Cannot determine head state for repo at `{}`",
34 work_dir.display()
35 )
36 })? {
37 bail!(
38 "Cannot operate on a detached head for repo at `{}`",
39 work_dir.display()
40 )
41 }
42
43 String::from(git_repo.head().with_context(|| {
44 format!(
45 "Cannot find the head branch for repo at `{}`. Is it detached?",
46 work_dir.display()
47 )
48 })?.shorthand().with_context(|| {
49 format!(
50 "Cannot find a human readable representation of the head ref for repo at `{}`",
51 work_dir.display(),
52 )
53 })?)
54 },
55 };
56
57 let subrepos = git_repo
58 .submodules()
59 .with_context(|| {
60 format!(
61 "Cannot load submodules for repo at `{}`",
62 work_dir.display()
63 )
64 })?
65 .iter()
66 .map(|submodule| Repo::new(&work_dir.join(submodule.path()), Some(&head)))
67 .collect::<Result<Vec<Repo>>>()?;
68
69 println!("Successfully read repo at `{}`", work_dir.display());
70
71 Ok(Repo {
72 git_repo,
73 work_dir: path::PathBuf::from(work_dir),
74 head,
75 subrepos,
76 })
77 }
78
79 pub fn get_subrepo_by_path(&self, subrepo_path: &path::PathBuf) -> Option<&Repo> {
80 self.subrepos
81 .iter()
82 .find(|subrepo| subrepo.work_dir == self.work_dir.join(subrepo_path))
83 }
84
85 pub fn sync(&self) -> Result<()> {
86 self.switch(&self.head)?;
87 Ok(())
88 }
89
90 pub fn switch(&self, head: &str) -> Result<()> {
91 self.git_repo.set_head(&self.resolve_reference(head)?)?;
92 self.git_repo.checkout_head(None)?;
93 Ok(())
94 }
95
96 pub fn fetch(&self) -> Result<()> {
97 let head_ref = self.git_repo.head()?;
99 let branch_name = head_ref.shorthand().with_context(|| {
100 format!(
101 "Cannot get branch name for repo at `{}`",
102 self.work_dir.display()
103 )
104 })?;
105
106 let remote_name = self.get_remote_name_for_branch(branch_name)?;
107
108 match self.git_repo.find_remote(&remote_name) {
110 Ok(mut remote) => {
111 let refspecs = &[format!(
112 "refs/heads/{}:refs/remotes/{}/{}",
113 branch_name, remote_name, branch_name
114 )];
115
116 remote.fetch(refspecs, None, None).with_context(|| {
117 format!(
118 "Failed to fetch from remote '{}' for repo at `{}`",
119 remote_name,
120 self.work_dir.display()
121 )
122 })?;
123 },
124 Err(_) => {
125 return Ok(());
127 },
128 }
129
130 Ok(())
131 }
132
133 pub fn merge(&self, branch_name: &str) -> Result<MergeResult> {
134 self.fetch()?;
136
137 let remote_name = self.get_remote_name_for_branch(branch_name)?;
139 let remote_branch_ref = format!("{}/{}", remote_name, branch_name);
140
141 let remote_branch_oid = match self.git_repo.refname_to_id(&remote_branch_ref) {
143 Ok(oid) => oid,
144 Err(_) => {
145 return Ok(MergeResult::UpToDate);
147 },
148 };
149
150 let remote_commit = self.git_repo.find_commit(remote_branch_oid)?;
151 let local_commit = self.git_repo.head()?.peel_to_commit()?;
152
153 if local_commit.id() == remote_commit.id() {
155 return Ok(MergeResult::UpToDate);
156 }
157
158 if self
160 .git_repo
161 .graph_descendant_of(local_commit.id(), remote_commit.id())?
162 {
163 self.git_repo
165 .set_head(&format!("refs/heads/{}", branch_name))?;
166 self.git_repo.checkout_head(None)?;
167 return Ok(MergeResult::FastForward);
168 }
169
170 let mut merge_opts = git2::MergeOptions::new();
172 merge_opts.fail_on_conflict(false); let _merge_result = self.git_repo.merge_commits(
175 &local_commit,
176 &remote_commit,
177 Some(&merge_opts),
178 )?;
179
180 let mut index = self.git_repo.index()?;
182 let has_conflicts = index.has_conflicts();
183
184 if !has_conflicts {
185 let signature = self.git_repo.signature()?;
187 let tree_id = index.write_tree()?;
188 let tree = self.git_repo.find_tree(tree_id)?;
189
190 self.git_repo.commit(
191 Some(&format!("refs/heads/{}", branch_name)),
192 &signature,
193 &signature,
194 &format!("Merge remote-tracking branch '{}'", remote_branch_ref),
195 &tree,
196 &[&local_commit, &remote_commit],
197 )?;
198
199 Ok(MergeResult::Merged)
200 } else {
201 Ok(MergeResult::Conflicts)
203 }
204 }
205
206 pub fn get_remote_name_for_branch(&self, _branch_name: &str) -> Result<String> {
207 Ok("origin".to_string())
210 }
211
212 fn resolve_reference(&self, short_name: &str) -> Result<String> {
213 Ok(self
214 .git_repo
215 .resolve_reference_from_short_name(short_name)?
216 .name()
217 .with_context(|| {
218 format!(
219 "Cannot resolve head reference for repo at `{}`",
220 self.work_dir.display()
221 )
222 })?
223 .to_owned())
224 }
225}
226
227impl fmt::Debug for Repo {
228 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
229 f.debug_struct("Repo")
230 .field("work_dir", &self.work_dir)
231 .field("head", &self.head)
232 .field("subrepos", &self.subrepos)
233 .finish()
234 }
235}