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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
use std::fs;
use std::path::{Path, PathBuf};

use crate::{GitCommitMeta, GitCredentials, GitRepo, GitRepoCloneRequest, GitRepoInfo};
use git_url_parse::GitUrl;

use git2::{Branch, Commit, Repository};

use color_eyre::eyre::{eyre, Result};
use tracing::debug;

impl GitRepo {
    /// Returns a `GitRepo` after parsing metadata from a repo
    /// - If a local `branch` is not provided, current checked out branch will be used.
    ///   The provided branch will be resolved to its remote branch name
    /// - If `commit_id` is not provided, the current commit (the HEAD of `branch`) will be used
    pub fn open(path: PathBuf, branch: Option<String>, commit_id: Option<String>) -> Result<Self> {
        // First we open the repository and get the remote_url and parse it into components
        let local_repo = Self::to_repository_from_path(path.clone())?;
        let remote_url = GitRepoInfo::git_remote_from_repo(&local_repo)?;

        // Resolve the remote branch name, if possible
        let working_branch_name = GitRepoInfo::get_git2_branch(&local_repo, &branch)?
            .name()?
            .map(str::to_string);

        // We don't support digging around in past commits if the repo is shallow
        if let Some(_c) = &commit_id {
            if local_repo.is_shallow() {
                return Err(eyre!("Can't open by commit on shallow clones"));
            }
        }

        let commit = Self::get_git2_commit(&local_repo, &working_branch_name, &commit_id)?;

        if let Some(url) = remote_url {
            Ok(Self::new(url)?
                .with_path(path)
                .with_branch(working_branch_name)
                .with_git2_commit(commit))
        } else {
            // Use this when the current branch has no remote ref
            let file_path = path.as_os_str().to_str().unwrap_or_default();
            Ok(Self::new(file_path)?
                .with_path(path)
                .with_branch(working_branch_name)
                .with_git2_commit(commit))
        }
    }

    /// Set the location of `GitRepo` on the filesystem
    pub fn with_path(mut self, path: PathBuf) -> Self {
        // We want to get the absolute path of the directory of the repo
        self.path = Some(fs::canonicalize(path).expect("Directory was not found"));
        self
    }

    /// Intended to be set with the remote name branch of GitRepo
    pub fn with_branch(mut self, branch: Option<String>) -> Self {
        if let Some(b) = branch {
            self.branch = Some(b);
        }
        self
    }

    /// Reinit `GitRepo` with commit id
    pub fn with_commit(mut self, commit_id: Option<String>) -> Self {
        self = Self::open(self.path.expect("No path set"), self.branch, commit_id)
            .expect("Unable to open GitRepo with commit id");
        self
    }

    /// Set the `GitCommitMeta` from `git2::Commit`
    pub fn with_git2_commit(mut self, commit: Option<Commit>) -> Self {
        match commit {
            Some(c) => {
                let commit_msg = c.message().unwrap_or_default().to_string();

                let commit = GitCommitMeta::new(c.id())
                    .with_message(Some(commit_msg))
                    .with_timestamp(c.time().seconds());

                self.head = Some(commit);
                self
            }
            None => {
                self.head = None;
                self
            }
        }
    }

    /// Set `GitCredentials` for private repos.
    /// `None` indicates public repo
    pub fn with_credentials(mut self, creds: Option<GitCredentials>) -> Self {
        self.credentials = creds;
        self
    }

    /// Create a new `GitRepo` with `url`.
    /// Use along with `with_*` methods to set other fields of `GitRepo`.
    /// Use `GitRepoCloner` if you need to clone the repo, and convert back with `GitRepo.into()`
    pub fn new<S: AsRef<str>>(url: S) -> Result<Self> {
        Ok(Self {
            url: GitUrl::parse(url.as_ref()).expect("url failed to parse as GitUrl"),
            credentials: None,
            head: None,
            branch: None,
            path: None,
        })
    }

    pub fn to_clone(&self) -> GitRepoCloneRequest {
        self.into()
    }

    pub fn to_info(&self) -> GitRepoInfo {
        self.into()
    }

    /// Returns a `git2::Repository` from `self.path`
    pub fn to_repository(&self) -> Result<Repository, git2::Error> {
        Self::to_repository_from_path(self.path.clone().expect("No path set to open").as_os_str())
    }

    /// Returns a `git2::Repository` from a given repo directory path
    pub fn to_repository_from_path<P: AsRef<Path>>(path: P) -> Result<Repository, git2::Error> {
        Repository::open(path.as_ref().as_os_str())
    }

    /// Return a `git2::Commit` that refers to the commit object requested for building
    /// If commit id is not provided, then we'll use the HEAD commit of whatever branch is active or provided
    fn get_git2_commit<'repo>(
        r: &'repo Repository,
        branch: &Option<String>,
        commit_id: &Option<String>,
    ) -> Result<Option<Commit<'repo>>> {
        let working_branch = GitRepoInfo::get_git2_branch(r, branch)?;

        match commit_id {
            Some(id) => {
                debug!("Commit provided. Using {}", id);
                let commit = r.find_commit(git2::Oid::from_str(id)?)?;

                // Do we care about detatched HEAD?
                //let _ = GitRepo::is_commit_in_branch(
                //    r,
                //    &commit,
                //    &Branch::wrap(working_branch.into_reference()),
                //);

                Ok(Some(commit))
            }

            // We want the HEAD of the remote branch (as opposed to the working branch)
            None => {
                debug!("No commit provided. Attempting to use HEAD commit from remote branch");

                match working_branch.upstream() {
                    Ok(upstream_branch) => {
                        let working_ref = upstream_branch.into_reference();

                        let commit = working_ref
                            .peel_to_commit()
                            .expect("Unable to retrieve HEAD commit object from remote branch");

                        let _ = GitRepoInfo::is_commit_in_branch(
                            r,
                            &commit,
                            &Branch::wrap(working_ref),
                        );

                        Ok(Some(commit))
                    }
                    // This match-arm supports branches that are local-only
                    Err(_e) => {
                        debug!("No remote branch found. Using HEAD commit from local branch");
                        let working_ref = working_branch.into_reference();

                        let commit = working_ref
                            .peel_to_commit()
                            .expect("Unable to retrieve HEAD commit object from local branch");

                        let _ = GitRepoInfo::is_commit_in_branch(
                            r,
                            &commit,
                            &Branch::wrap(working_ref),
                        );

                        Ok(Some(commit))
                    }
                }
            }
        }
    }

    /// Test whether `GitRepo` is a shallow clone
    pub fn is_shallow(&self) -> bool {
        let repo = self.to_repository().expect("Could not read repo");
        repo.is_shallow()
    }
}