git_branch_stash/
git.rs

1use bstr::ByteSlice;
2
3#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4pub(crate) struct Branch {
5    pub(crate) name: String,
6    pub(crate) id: git2::Oid,
7    pub(crate) push_id: Option<git2::Oid>,
8    pub(crate) pull_id: Option<git2::Oid>,
9}
10
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub(crate) struct Commit {
13    pub(crate) id: git2::Oid,
14    pub(crate) tree_id: git2::Oid,
15    pub(crate) summary: bstr::BString,
16    pub(crate) time: std::time::SystemTime,
17    pub(crate) author: Option<std::rc::Rc<str>>,
18    pub(crate) committer: Option<std::rc::Rc<str>>,
19}
20
21pub struct GitRepo {
22    repo: git2::Repository,
23    push_remote: Option<String>,
24    pull_remote: Option<String>,
25    commits: std::cell::RefCell<std::collections::HashMap<git2::Oid, std::rc::Rc<Commit>>>,
26    interned_strings: std::cell::RefCell<std::collections::HashSet<std::rc::Rc<str>>>,
27}
28
29impl GitRepo {
30    pub fn new(repo: git2::Repository) -> Self {
31        Self {
32            repo,
33            push_remote: None,
34            pull_remote: None,
35            commits: Default::default(),
36            interned_strings: Default::default(),
37        }
38    }
39
40    pub(crate) fn push_remote(&self) -> &str {
41        self.push_remote.as_deref().unwrap_or("origin")
42    }
43
44    pub(crate) fn pull_remote(&self) -> &str {
45        self.pull_remote.as_deref().unwrap_or("origin")
46    }
47
48    pub fn raw(&self) -> &git2::Repository {
49        &self.repo
50    }
51
52    pub fn raw_mut(&mut self) -> &mut git2::Repository {
53        &mut self.repo
54    }
55
56    pub(crate) fn find_commit(&self, id: git2::Oid) -> Option<std::rc::Rc<Commit>> {
57        let mut commits = self.commits.borrow_mut();
58        if let Some(commit) = commits.get(&id) {
59            Some(std::rc::Rc::clone(commit))
60        } else {
61            let commit = self.repo.find_commit(id).ok()?;
62            let summary: bstr::BString = commit.summary_bytes().unwrap().into();
63            let time = std::time::SystemTime::UNIX_EPOCH
64                + std::time::Duration::from_secs(commit.time().seconds().max(0) as u64);
65
66            let author = commit.author().name().map(|n| self.intern_string(n));
67            let committer = commit.author().name().map(|n| self.intern_string(n));
68            let commit = std::rc::Rc::new(Commit {
69                id: commit.id(),
70                tree_id: commit.tree_id(),
71                summary,
72                time,
73                author,
74                committer,
75            });
76            commits.insert(id, std::rc::Rc::clone(&commit));
77            Some(commit)
78        }
79    }
80
81    pub(crate) fn head_branch(&self) -> Option<Branch> {
82        let resolved = self.repo.head().unwrap().resolve().unwrap();
83        let name = resolved.shorthand()?;
84        let id = resolved.target()?;
85
86        let push_id = self
87            .repo
88            .find_branch(
89                &format!("{}/{}", self.push_remote(), name),
90                git2::BranchType::Remote,
91            )
92            .ok()
93            .and_then(|b| b.get().target());
94        let pull_id = self
95            .repo
96            .find_branch(
97                &format!("{}/{}", self.pull_remote(), name),
98                git2::BranchType::Remote,
99            )
100            .ok()
101            .and_then(|b| b.get().target());
102
103        Some(Branch {
104            name: name.to_owned(),
105            id,
106            push_id,
107            pull_id,
108        })
109    }
110
111    pub(crate) fn branch(&mut self, name: &str, id: git2::Oid) -> Result<(), git2::Error> {
112        let commit = self.repo.find_commit(id)?;
113        self.repo.branch(name, &commit, true)?;
114        Ok(())
115    }
116
117    pub(crate) fn find_local_branch(&self, name: &str) -> Option<Branch> {
118        let branch = self.repo.find_branch(name, git2::BranchType::Local).ok()?;
119        let id = branch.get().target().unwrap();
120
121        let push_id = self
122            .repo
123            .find_branch(
124                &format!("{}/{}", self.push_remote(), name),
125                git2::BranchType::Remote,
126            )
127            .ok()
128            .and_then(|b| b.get().target());
129        let pull_id = self
130            .repo
131            .find_branch(
132                &format!("{}/{}", self.pull_remote(), name),
133                git2::BranchType::Remote,
134            )
135            .ok()
136            .and_then(|b| b.get().target());
137
138        Some(Branch {
139            name: name.to_owned(),
140            id,
141            push_id,
142            pull_id,
143        })
144    }
145
146    pub(crate) fn local_branches(&self) -> impl Iterator<Item = Branch> + '_ {
147        log::trace!("Loading branches");
148        self.repo
149            .branches(Some(git2::BranchType::Local))
150            .into_iter()
151            .flatten()
152            .flat_map(move |branch| {
153                let (branch, _) = branch.ok()?;
154                let name = if let Some(name) = branch.name().ok().flatten() {
155                    name
156                } else {
157                    log::debug!(
158                        "Ignoring non-UTF8 branch {:?}",
159                        branch.name_bytes().unwrap().as_bstr()
160                    );
161                    return None;
162                };
163                let id = branch.get().target().unwrap();
164
165                let push_id = self
166                    .repo
167                    .find_branch(
168                        &format!("{}/{}", self.push_remote(), name),
169                        git2::BranchType::Remote,
170                    )
171                    .ok()
172                    .and_then(|b| b.get().target());
173                let pull_id = self
174                    .repo
175                    .find_branch(
176                        &format!("{}/{}", self.pull_remote(), name),
177                        git2::BranchType::Remote,
178                    )
179                    .ok()
180                    .and_then(|b| b.get().target());
181
182                Some(Branch {
183                    name: name.to_owned(),
184                    id,
185                    push_id,
186                    pull_id,
187                })
188            })
189    }
190
191    pub(crate) fn detach(&mut self) -> Result<(), git2::Error> {
192        let head_id = self
193            .repo
194            .head()
195            .unwrap()
196            .resolve()
197            .unwrap()
198            .target()
199            .unwrap();
200        self.repo.set_head_detached(head_id)?;
201        Ok(())
202    }
203
204    pub(crate) fn switch(&mut self, name: &str) -> Result<(), git2::Error> {
205        // HACK: We shouldn't limit ourselves to `Local`
206        let branch = self.repo.find_branch(name, git2::BranchType::Local)?;
207        self.repo.set_head(branch.get().name().unwrap())?;
208        let mut builder = git2::build::CheckoutBuilder::new();
209        builder.force();
210        self.repo.checkout_head(Some(&mut builder))?;
211        Ok(())
212    }
213
214    fn intern_string(&self, data: &str) -> std::rc::Rc<str> {
215        let mut interned_strings = self.interned_strings.borrow_mut();
216        if let Some(interned) = interned_strings.get(data) {
217            std::rc::Rc::clone(interned)
218        } else {
219            let interned = std::rc::Rc::from(data);
220            interned_strings.insert(std::rc::Rc::clone(&interned));
221            interned
222        }
223    }
224}
225
226impl std::fmt::Debug for GitRepo {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
228        f.debug_struct("GitRepo")
229            .field("repo", &self.repo.workdir())
230            .field("push_remote", &self.push_remote.as_deref())
231            .field("pull_remote", &self.pull_remote.as_deref())
232            .finish()
233    }
234}