Skip to main content

git_branch_status/
repository_ext.rs

1// Copyright 2021 Akiomi Kamakura
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fs;
16
17use git2::{Error, ErrorCode, Oid, Repository, RepositoryState, StatusOptions};
18
19use crate::branch_status::BranchStatus;
20use crate::status_entry_ext::StatusEntryExt;
21
22pub trait RepositoryExt {
23    fn action(&self) -> Option<&str>;
24    fn branch_name(&self) -> Result<String, Error>;
25    fn branch_status(&self) -> Result<BranchStatus, Error>;
26    fn rebase_i_head_name(&self) -> Result<String, Error>;
27    fn to_short_oid(&self, oid: Oid) -> Result<Option<String>, Error>;
28}
29
30impl RepositoryExt for Repository {
31    fn action(&self) -> Option<&str> {
32        match self.state() {
33            RepositoryState::ApplyMailbox => Some("am"),
34            RepositoryState::ApplyMailboxOrRebase => Some("am/rebase"),
35            RepositoryState::Bisect => Some("bisect"),
36            RepositoryState::CherryPick => Some("cherry"),
37            RepositoryState::CherryPickSequence => Some("cherry-seq"),
38            RepositoryState::Merge => Some("merge"),
39            RepositoryState::Rebase => Some("rebase"),
40            RepositoryState::RebaseInteractive => Some("rebase-i"),
41            RepositoryState::RebaseMerge => Some("rebase-m"),
42            RepositoryState::Revert => Some("revert"),
43            RepositoryState::RevertSequence => Some("revert-seq"),
44            _ => None,
45        }
46    }
47
48    fn branch_name(&self) -> Result<String, Error> {
49        let head = match self.head() {
50            Ok(head) => Some(head),
51            Err(ref e)
52                if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound =>
53            {
54                None
55            }
56            Err(e) => return Err(e),
57        };
58
59        let detached = self.head_detached()?;
60
61        let branch = if self.state() == RepositoryState::RebaseInteractive {
62            self.rebase_i_head_name()?
63        } else if detached {
64            let oid = head.as_ref().and_then(|h| h.target());
65            let short = match oid.map(|oid| self.to_short_oid(oid)) {
66                Some(Ok(id)) => id,
67                Some(Err(e)) => return Err(e),
68                None => None,
69            };
70
71            short.unwrap_or_else(|| "HEAD (detached)".to_string())
72        } else {
73            head.as_ref()
74                .and_then(|h| h.shorthand().ok())
75                .unwrap_or("HEAD (no branch)")
76                .to_string()
77        };
78
79        match self.action() {
80            Some(action) => Ok(branch + ":" + action),
81            None => Ok(branch),
82        }
83    }
84
85    fn branch_status(&self) -> Result<BranchStatus, Error> {
86        let mut opts = StatusOptions::new();
87        opts.include_untracked(false)
88            .include_ignored(false)
89            .include_unmodified(false)
90            .exclude_submodules(true);
91
92        let stats = self.statuses(Some(&mut opts))?;
93
94        let status = stats.iter().fold(BranchStatus::NotChanged, |acc, s| {
95            if acc < BranchStatus::Conflicted && s.is_conflicted() {
96                BranchStatus::Conflicted
97            } else if acc < BranchStatus::Unstaged && s.is_unstaged() {
98                BranchStatus::Unstaged
99            } else if acc < BranchStatus::Staged && s.is_staged() {
100                BranchStatus::Staged
101            } else {
102                acc
103            }
104        });
105
106        Ok(status)
107    }
108
109    fn rebase_i_head_name(&self) -> Result<String, Error> {
110        let path = self.path().join("rebase-merge").join("head-name");
111
112        let refname = match fs::read_to_string(path) {
113            Ok(content) => content,
114            Err(e) => return Err(Error::from_str(&e.to_string())),
115        };
116
117        match self.find_reference(refname.trim()) {
118            Ok(ref reference) => Ok(reference.shorthand().unwrap_or(&refname).to_string()),
119            Err(e) => Err(e),
120        }
121    }
122
123    fn to_short_oid(&self, oid: Oid) -> Result<Option<String>, Error> {
124        let object = self.find_object(oid, None)?;
125        match object.short_id() {
126            Ok(id) => Ok(id.as_str().map(|i| i.to_string()).ok()),
127            Err(e) => Err(e),
128        }
129    }
130}