git_trim/
branch.rs

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}