aboutsummaryrefslogtreecommitdiff
path: root/src/repo/git/mod.rs
blob: 24b1e1ad24293ac18570b45e9fd6a813ef216af0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use super::{LocalRepoState, Repo, RepoError};

use anyhow::Context;
use gix::{
    bstr::{BString, ByteSlice},
    refs::{
        transaction::{LogChange, PreviousValue, RefEdit},
        FullName,
    },
    remote, Id, ObjectId, Reference, Remote,
};
use tracing::debug;

mod checkout;
mod fetch;
mod ffmerge;

impl Repo {
    #[tracing::instrument(level = "debug")]
    pub fn is_clean(&self) -> Result<LocalRepoState, RepoError> {
        let repo = self.repo()?;

        if let Some(state) = repo.state() {
            Ok(LocalRepoState::InProgress(state))
        } else {
            let head = repo.head().unwrap();

            if head.is_detached() {
                return Ok(LocalRepoState::DetachedHead);
            }

            if head.is_unborn() {
                return Ok(LocalRepoState::UnbornHead);
            }

            let head = self.repo()?.head().unwrap();
            let branch = head.referent_name().unwrap();
            let default_branch = self.default_branch()?;

            if !branch.as_bstr().contains_str(default_branch) {
                return Ok(LocalRepoState::NonDefaultBranch);
            }

            let default_ref = self.default_remote_ref()?.into_fully_peeled_id().unwrap();

            let head_ref = repo
                .head_ref()
                .map_err(|_| RepoError::NoHead)?
                .ok_or(RepoError::NoHead)?
                .into_fully_peeled_id()
                .unwrap();

            let unpushed_commits = head_ref
                .ancestors()
                .with_boundary([default_ref])
                .all()
                .unwrap()
                .count();

            if default_ref != head_ref && unpushed_commits > 0 {
                return Ok(LocalRepoState::UnpushedCommits(unpushed_commits));
            }

            Ok(LocalRepoState::Clean)
        }
    }

    pub fn default_remote(&self) -> Result<Remote, RepoError> {
        Ok(self
            .repo()?
            .find_default_remote(gix::remote::Direction::Fetch)
            .ok_or(RepoError::NoRemoteFound)?
            .context("fetch: failed to find default remote")?)
    }

    pub fn default_remote_ref(&self) -> Result<Reference, RepoError> {
        let repo = self.repo()?;
        let remote = self.default_remote()?;
        let remote_name = remote.name().context("remote does not have name")?;

        let origin_ref = repo
            .find_reference(&format!("remotes/{}/HEAD", remote_name.as_bstr()))
            .context("the remotes HEAD references does not exist")?;

        debug!("got ref to origin: {:?}", origin_ref);

        Ok(origin_ref)
    }

    pub fn default_branch(&self) -> Result<BString, RepoError> {
        let remote = self.default_remote()?;
        let remote_name = remote.name().context("remote does not have name")?;
        let origin_ref = self.default_remote_ref()?;

        if let Some(origin_ref) = origin_ref.target().try_name() {
            let shortened = origin_ref.shorten().to_string();

            let strip: String = format!("{}/", remote_name.as_bstr());
            Ok(shortened.strip_prefix(&strip).unwrap().into())
        } else {
            Err(RepoError::NoDefaultBranch)
        }
    }

    pub fn refedit(target: ObjectId, name: &str, message: &str) -> RefEdit {
        RefEdit {
            change: gix::refs::transaction::Change::Update {
                log: LogChange {
                    mode: gix::refs::transaction::RefLog::AndReference,
                    force_create_reflog: false,
                    message: message.into(),
                },
                expected: PreviousValue::Any,
                new: gix::refs::Target::Object(target),
            },
            name: FullName::try_from(name).unwrap(),
            deref: true,
        }
    }

    pub fn update_default_branch_ref(
        &self,
        remote: &remote::Name,
        head: Id,
    ) -> Result<(), RepoError> {
        let default_branch = self.default_branch()?;
        debug!("default branch: {:?}", default_branch);

        let repo = self.repo()?;

        let edits = repo
            .edit_reference(Repo::refedit(
                head.into(),
                &format!("refs/heads/{}", default_branch),
                &format!("checkout: {}/HEAD with gtree", remote.as_bstr()),
            ))
            .context("checkout: failed to edit ref")?;

        debug!("ref edits: {:?}", edits);

        Ok(())
    }

    pub fn default_remote_head(&self) -> Result<(remote::Name, Id), RepoError> {
        let repo = self.repo()?;

        let remote = repo
            .find_fetch_remote(None)
            .context("could not find remote to fetch")?;
        let remote = remote.name().context("remote does not have name")?;

        let head_ref = repo
            .find_reference(&format!("remotes/{}/HEAD", remote.as_bstr()))
            .context("the remotes HEAD references does not exist")?;
        let head = head_ref
            .into_fully_peeled_id()
            .context("failed to peel ref")?;

        Ok((remote.to_owned(), head.to_owned()))
    }
}