1
use std::collections::HashSet;
2
use std::fmt::{Debug, Display};
3

            
4
use anyhow::{bail, Context, Result};
5
use git2::Reference;
6

            
7
/// A wrapper around git2::Branch.
8
pub struct Branch<'a>(git2::Branch<'a>);
9

            
10
impl Branch<'_> {
11
    /// Delete the git branch.
12
    ///
13
    /// ## Errors
14
    ///
15
    /// Returns GitSnipError::DeleteBranchError if the branch cannot be deleted.
16
8
    pub fn delete(&mut self) -> Result<()> {
17
8
        self.0.delete().context("Failed to delete branch")
18
8
    }
19

            
20
    /// Return the name of the branch.
21
74
    pub fn name(&self) -> Result<String> {
22
74
        match self.0.name() {
23
74
            Ok(Some(name)) => Ok(name.to_string()),
24
            Ok(None) => bail!("Branch has no name"),
25
            Err(e) => bail!("Failed to get branch name: {}", e),
26
        }
27
74
    }
28

            
29
    /// Remove a branch name prefix from a list of possible prefixes.
30
    /// If none match, return the original branch name.
31
4
    pub fn name_without_prefix(&self, prefixes: &HashSet<String>) -> Result<String> {
32
4
        let name = self.name().context("Branch has no name")?;
33
4
        for prefix in prefixes {
34
4
            if name.starts_with(prefix) {
35
2
                return Ok(name[prefix.len()..].to_string());
36
2
            }
37
        }
38
2
        Ok(name)
39
4
    }
40
}
41

            
42
impl<'a> From<git2::Branch<'a>> for Branch<'a> {
43
40
    fn from(branch: git2::Branch<'a>) -> Self {
44
40
        Branch(branch)
45
40
    }
46
}
47

            
48
// Optionally, allow conversion from your own Reference wrapper if desired.
49
impl<'a> From<Reference<'a>> for Branch<'a> {
50
    fn from(reference: Reference<'a>) -> Self {
51
        Branch(git2::Branch::wrap(reference))
52
    }
53
}
54

            
55
impl PartialEq for Branch<'_> {
56
2
    fn eq(&self, other: &Self) -> bool {
57
2
        self.name().ok() == other.name().ok()
58
2
    }
59
}
60

            
61
impl Eq for Branch<'_> {}
62

            
63
impl Debug for Branch<'_> {
64
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65
        f.debug_struct("Branch")
66
            .field("name", &self.name())
67
            .finish()
68
    }
69
}
70

            
71
impl Display for Branch<'_> {
72
42
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73
42
        match self.name() {
74
42
            Ok(name) => write!(f, "{name}"),
75
            Err(_) => write!(f, "<invalid branch>"),
76
        }
77
42
    }
78
}
79

            
80
#[cfg(test)]
81
mod tests {
82
    use super::*;
83
    use crate::test_utilities;
84

            
85
    #[test]
86
2
    fn test_name() {
87
        // GIVEN a mock repository with a branch
88
2
        let (_testdir, repo) = test_utilities::create_mock_repo();
89
2
        let branch_name = "test-branch";
90
2
        let target_commit = test_utilities::get_latest_commit(&repo);
91
2
        let branch = repo.branch(branch_name, &target_commit, false).unwrap();
92

            
93
        // WHEN we get the name of the branch
94
2
        let actual = Branch::from(branch).name().unwrap();
95

            
96
        // THEN the name should match the branch name
97
2
        assert_eq!(actual, branch_name);
98
2
    }
99

            
100
    #[test]
101
2
    fn test_name_without_prefix() {
102
        // GIVEN a mock repository with a branch that has a remote prefix
103
2
        let (_testdir, repo) = test_utilities::create_mock_repo();
104
2
        let branch_name = "origin/test-branch";
105
2
        let target_commit = test_utilities::get_latest_commit(&repo);
106
2
        let branch = repo.branch(branch_name, &target_commit, false).unwrap();
107

            
108
        // WHEN we get the name without the remote prefix
109
2
        let remote_prefixes = HashSet::from_iter(vec!["origin/".to_string()]);
110
2
        let actual = Branch::from(branch)
111
2
            .name_without_prefix(&remote_prefixes)
112
2
            .unwrap();
113

            
114
        // THEN the name should be the branch name without the prefix
115
2
        assert_eq!(actual, "test-branch");
116
2
    }
117

            
118
    #[test]
119
2
    fn test_name_without_prefix_no_prefix() {
120
        // GIVEN a mock repository with a branch that does not have a remote prefix
121
2
        let (_testdir, repo) = test_utilities::create_mock_repo();
122
2
        let branch_name = "test-branch";
123
2
        let target_commit = test_utilities::get_latest_commit(&repo);
124
2
        let branch = repo.branch(branch_name, &target_commit, false).unwrap();
125

            
126
        // WHEN we get the name without the remote prefix
127
2
        let remote_prefixes = HashSet::from_iter(vec!["origin/".to_string()]);
128
2
        let actual = Branch::from(branch)
129
2
            .name_without_prefix(&remote_prefixes)
130
2
            .unwrap();
131

            
132
        // THEN the name should be the branch name as it has no prefix
133
2
        assert_eq!(actual, "test-branch");
134
2
    }
135

            
136
    #[test]
137
2
    fn test_delete() {
138
        // GIVEN a mock repository with a branch
139
2
        let (_testdir, repo) = test_utilities::create_mock_repo();
140
2
        let branch_name = "test-branch";
141
2
        let target_commit = test_utilities::get_latest_commit(&repo);
142
2
        let branch = repo.branch(branch_name, &target_commit, false);
143

            
144
        // WHEN we delete the branch
145
2
        let mut branch = Branch::from(branch.unwrap());
146
2
        let _ = branch.delete();
147

            
148
        // THEN the branch should no longer exist
149
2
        assert!(repo
150
2
            .find_branch(branch_name, git2::BranchType::Local)
151
2
            .is_err());
152
2
    }
153

            
154
    #[test]
155
2
    fn test_branch_equality() {
156
        // GIVEN a mock repository with two branches of the same name
157
2
        let (_testdir, repo) = test_utilities::create_mock_repo();
158
2
        let branch_name = "test-branch";
159
2
        let target_commit = test_utilities::get_latest_commit(&repo);
160

            
161
        // WHEN we create two branches with the same name
162
2
        let branch1 = repo.branch(branch_name, &target_commit, false).unwrap();
163
2
        let branch2 = repo
164
2
            .find_branch(branch_name, git2::BranchType::Local)
165
2
            .unwrap();
166

            
167
        // THEN both branches should be equal
168
2
        assert_eq!(Branch::from(branch1), Branch::from(branch2));
169
2
    }
170
}