1use std::convert::TryFrom;
2
3use anyhow::{Context, Result};
4use git2::{Branch, Config, Direction, Reference, Repository};
5use log::*;
6use thiserror::Error;
7
8use crate::config;
9use crate::simple_glob::{expand_refspec, ExpansionSide};
10
11pub trait Refname {
12 fn refname(&self) -> &str;
13}
14
15#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Clone)]
16pub struct LocalBranch {
17 pub refname: String,
18}
19
20impl LocalBranch {
21 pub fn new(refname: &str) -> Self {
22 assert!(refname.starts_with("refs/heads/"));
23 Self {
24 refname: refname.to_string(),
25 }
26 }
27
28 pub fn short_name(&self) -> &str {
29 &self.refname["refs/heads/".len()..]
30 }
31
32 pub fn fetch_upstream(
33 &self,
34 repo: &Repository,
35 config: &Config,
36 ) -> Result<RemoteTrackingBranchStatus> {
37 let remote_name = if let Some(remote_name) = config::get_remote_name(config, self)? {
38 remote_name
39 } else {
40 return Ok(RemoteTrackingBranchStatus::None);
41 };
42 let merge: String = if let Some(merge) = config::get_merge(config, self)? {
43 merge
44 } else {
45 return Ok(RemoteTrackingBranchStatus::None);
46 };
47
48 RemoteTrackingBranch::from_remote_branch(
49 repo,
50 &RemoteBranch {
51 remote: remote_name,
52 refname: merge,
53 },
54 )
55 }
56}
57
58impl Refname for LocalBranch {
59 fn refname(&self) -> &str {
60 &self.refname
61 }
62}
63
64impl<'repo> TryFrom<&git2::Branch<'repo>> for LocalBranch {
65 type Error = anyhow::Error;
66
67 fn try_from(branch: &Branch<'repo>) -> Result<Self> {
68 let refname = branch.get().name().context("non-utf8 branch ref")?;
69 Ok(Self::new(refname))
70 }
71}
72
73impl<'repo> TryFrom<&git2::Reference<'repo>> for LocalBranch {
74 type Error = anyhow::Error;
75
76 fn try_from(reference: &Reference<'repo>) -> Result<Self> {
77 if !reference.is_branch() {
78 anyhow::bail!("Reference {:?} is not a branch", reference.name());
79 }
80
81 let refname = reference.name().context("non-utf8 reference name")?;
82 Ok(Self::new(refname))
83 }
84}
85
86#[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Clone)]
87pub struct RemoteTrackingBranch {
88 pub refname: String,
89}
90
91impl RemoteTrackingBranch {
92 pub fn new(refname: &str) -> RemoteTrackingBranch {
93 assert!(refname.starts_with("refs/remotes/"));
94 RemoteTrackingBranch {
95 refname: refname.to_string(),
96 }
97 }
98
99 pub fn from_remote_branch(
100 repo: &Repository,
101 remote_branch: &RemoteBranch,
102 ) -> Result<RemoteTrackingBranchStatus> {
103 let remote = config::get_remote(repo, &remote_branch.remote)?;
104 if let Some(remote) = remote {
105 let refname = if let Some(expanded) = expand_refspec(
106 &remote,
107 &remote_branch.refname,
108 Direction::Fetch,
109 ExpansionSide::Right,
110 )? {
111 expanded
112 } else {
113 return Ok(RemoteTrackingBranchStatus::None);
114 };
115
116 if repo.find_reference(&refname).is_ok() {
117 return Ok(RemoteTrackingBranchStatus::Exists(
118 RemoteTrackingBranch::new(&refname),
119 ));
120 } else {
121 return Ok(RemoteTrackingBranchStatus::Gone(refname));
122 }
123 }
124 Ok(RemoteTrackingBranchStatus::None)
125 }
126
127 pub fn to_remote_branch(
128 &self,
129 repo: &Repository,
130 ) -> std::result::Result<RemoteBranch, RemoteBranchError> {
131 for remote_name in repo.remotes()?.iter() {
132 let remote_name = remote_name.context("non-utf8 remote name")?;
133 let remote = repo.find_remote(remote_name)?;
134 if let Some(expanded) = expand_refspec(
135 &remote,
136 &self.refname,
137 Direction::Fetch,
138 ExpansionSide::Left,
139 )? {
140 return Ok(RemoteBranch {
141 remote: remote.name().context("non-utf8 remote name")?.to_string(),
142 refname: expanded,
143 });
144 }
145 }
146 Err(RemoteBranchError::RemoteNotFound)
147 }
148}
149
150impl Refname for RemoteTrackingBranch {
151 fn refname(&self) -> &str {
152 &self.refname
153 }
154}
155
156impl<'repo> TryFrom<&git2::Branch<'repo>> for RemoteTrackingBranch {
157 type Error = anyhow::Error;
158
159 fn try_from(branch: &Branch<'repo>) -> Result<Self> {
160 let refname = branch.get().name().context("non-utf8 branch ref")?;
161 Ok(Self::new(refname))
162 }
163}
164
165impl<'repo> TryFrom<&git2::Reference<'repo>> for RemoteTrackingBranch {
166 type Error = anyhow::Error;
167
168 fn try_from(reference: &Reference<'repo>) -> Result<Self> {
169 if !reference.is_remote() {
170 anyhow::bail!("Reference {:?} is not a branch", reference.name());
171 }
172
173 let refname = reference.name().context("non-utf8 reference name")?;
174 Ok(Self::new(refname))
175 }
176}
177
178pub enum RemoteTrackingBranchStatus {
179 Exists(RemoteTrackingBranch),
180 Gone(String),
181 None,
182}
183
184#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Debug)]
185pub struct RemoteBranch {
186 pub remote: String,
187 pub refname: String,
188}
189
190impl std::fmt::Display for RemoteBranch {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(f, "{}, {}", self.remote, self.refname)
193 }
194}
195
196#[derive(Error, Debug)]
197pub enum RemoteBranchError {
198 #[error("anyhow error")]
199 AnyhowError(#[from] anyhow::Error),
200 #[error("libgit2 internal error")]
201 GitError(#[from] git2::Error),
202 #[error("remote with matching refspec not found")]
203 RemoteNotFound,
204}