Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reuse a git repository when it was previously cloned #158

Merged
merged 2 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/project/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub enum ProjectType {
pub async fn prepare_project(
location: &Path,
force_type: Option<ProjectType>,
options: Option<Options>,
options: Options,
) -> Result<PathBuf> {
let project_type = if force_type.is_some() {
force_type.unwrap()
Expand Down
159 changes: 118 additions & 41 deletions crates/project/src/types/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@

use crate::options::{GitReference, Options};
use anyhow::{anyhow, bail, Result};
use git2::{Oid, Repository};
use git2::{build::CheckoutBuilder, FetchOptions, Oid, Repository};
use sha256::digest as sha256_digest;
use std::{
env::temp_dir,
fs::remove_dir_all,
path::{Path, PathBuf},
};

Expand All @@ -16,58 +15,136 @@ static DEFAULT_REMOTE: &str = "origin";

/// Prepare a project based on a git repository. This method
/// clones the repo locally and returns the path in which it's located.
pub fn prepare_git_project(location: &Path, options: Option<Options>) -> Result<PathBuf> {
pub fn prepare_git_project(location: &Path, options: Options) -> Result<PathBuf> {
let project_url = location
.to_str()
.ok_or(anyhow!("The project URL cannot be retrieved"))?;
let (folder, git_ref) = parse_options(options);
// By default, we use temporary dirs
let mut dir = temp_dir().join(sha256_digest(project_url));

if dir.exists() {
// Clean up a previous download
remove_dir_all(&dir)?;
}

let repo = match Repository::clone(project_url, &dir) {
Ok(repo) => repo,
Err(e) => bail!("There was an error cloning the repository: {e}"),
let repo = if dir.exists() {
// Reuse the same repository.
Repository::open(&dir)
.map_err(|e| anyhow!("There was an error opening the repository: {e}"))?
Angelmmiguel marked this conversation as resolved.
Show resolved Hide resolved
} else {
// clone it
Repository::clone(project_url, &dir)
.map_err(|e| anyhow!("There was an error cloning the repository: {e}"))?
Angelmmiguel marked this conversation as resolved.
Show resolved Hide resolved
};

if let Some(options) = options {
if let Some(git) = options.git {
if let Some(git_ref) = git.git_ref {
match git_ref {
GitReference::Commit(commit) => {
let oid = Oid::from_str(&commit)?;
let commit = repo.find_commit(oid)?;
repo.checkout_tree(commit.as_object(), None)?;
}
GitReference::Tag(tag) => {
let mut remote = repo.find_remote(DEFAULT_REMOTE)?;
let tag_remote = format!("refs/tags/{tag}:refs/tags/{tag}");
remote.fetch(&[&tag_remote], None, None)?;

let oid = Oid::from_str(&tag)?;
let tag = repo.find_tag(oid)?;
repo.checkout_tree(tag.as_object(), None)?;
}
GitReference::Branch(branch) => {
let mut remote = repo.find_remote(DEFAULT_REMOTE)?;
let head_remote = format!("refs/heads/{branch}:refs/heads/{branch}");
remote.fetch(&[&head_remote], None, None)?;

let branch = repo.find_branch(&branch, git2::BranchType::Local)?;
let reference = branch.into_reference();
repo.checkout_tree(&reference.peel(git2::ObjectType::Tree)?, None)?;
}
}
if let Some(git_ref) = git_ref.as_ref() {
match git_ref {
GitReference::Commit(commit) => {
pull_default_branch(&repo)?;

let oid = Oid::from_str(commit)?;
repo.set_head_detached(oid)?;
repo.checkout_head(Some(&mut default_checkout()))?;
}
GitReference::Tag(tag) => {
let mut remote = repo.find_remote(DEFAULT_REMOTE)?;
let tag_remote = format!("refs/tags/{tag}:refs/tags/{tag}");
remote.fetch(&[&tag_remote], None, None)?;

repo.set_head(&format!("refs/tags/{tag}"))?;
repo.checkout_head(Some(&mut default_checkout()))?;
}
GitReference::Branch(branch) => {
let mut remote = repo.find_remote(DEFAULT_REMOTE)?;
let head_remote = format!("refs/heads/{branch}:refs/heads/{branch}");
remote.fetch(&[&head_remote], None, None)?;

if let Some(folder) = git.folder {
dir = dir.join(folder);
repo.set_head(&format!("refs/heads/{branch}"))?;
repo.checkout_head(Some(&mut default_checkout()))?;
}
}
} else {
pull_default_branch(&repo)?;
}

if let Some(folder) = folder {
dir = dir.join(folder);
}

Ok(dir)
}

/// Generates a default configuration to checkout the git repository
fn default_checkout<'cb>() -> CheckoutBuilder<'cb> {
let mut checkout_builder = CheckoutBuilder::default();

checkout_builder
.allow_conflicts(true)
.conflict_style_merge(true)
.force();

checkout_builder
}

/// Parse the different configuration parameters from the given Options
fn parse_options(options: Options) -> (Option<String>, Option<GitReference>) {
if let Some(git) = options.git {
(git.folder, git.git_ref)
} else {
(None, None)
}
}

/// Pull the changes from the default branch
fn pull_default_branch(repo: &Repository) -> Result<()> {
let branch = detect_main_branch(repo)?;
pull_repository(repo, branch)
}

/// Detech the main branch of this repository
fn detect_main_branch(repo: &Repository) -> Result<&str> {
// For now, we only distinguish between the two most common branch names.
// Ask the user to set the branch in any other case.
if repo.find_branch("main", git2::BranchType::Local).is_ok() {
Ok("main")
} else if repo.find_branch("master", git2::BranchType::Local).is_ok() {
Ok("master")
} else {
bail!("Couldn't find the default main branch. Please, set the Git branch you want to use.")
}
}
Angelmmiguel marked this conversation as resolved.
Show resolved Hide resolved

/// Fetch the latest references from a repository and pull all mising
/// objects. This method ensures an existing repo is not stale
fn pull_repository(repo: &Repository, branch: &str) -> Result<()> {
let mut remote = repo.find_remote(DEFAULT_REMOTE)?;
let mut fo = FetchOptions::new();

remote.fetch(&[branch], Some(&mut fo), None)?;

let fetch_head = repo.find_reference("FETCH_HEAD")?;
let fetch_commit = fetch_head.peel_to_commit()?;

// Follow a fast-forward merge by default. These repositories shouldn't be
// modified. In any other case, it will fail.
let refname = format!("refs/heads/{}", branch);

match repo.find_reference(&refname) {
Ok(mut reference) => {
// Get the reference name
let name = match reference.name() {
Some(s) => s.to_string(),
None => String::from_utf8_lossy(reference.name_bytes()).to_string(),
};

// Perform the pull
reference.set_target(fetch_commit.id(), "")?;
repo.set_head(&name)?;
repo.checkout_head(Some(&mut default_checkout()))?;
}
Err(_) => {
// The branch doesn't exist
repo.reference(&refname, fetch_commit.id(), true, "")?;
repo.set_head(&refname)?;
repo.checkout_head(Some(&mut default_checkout()))?;
}
};

Ok(())
}
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ async fn main() -> std::io::Result<()> {
let project_opts = options::build_project_options(&args);

println!("⚙️ Preparing the project from: {}", &args.path.display());
let project_path = match prepare_project(&args.path, None, Some(project_opts)).await {
let project_path = match prepare_project(&args.path, None, project_opts).await {
Ok(p) => p,
Err(err) => {
eprintln!("❌ There was an error preparing the project: {err}");
Expand Down