Skip to content

Commit

Permalink
Merge pull request #104 from siketyan/feat/fork-before-clone
Browse files Browse the repository at this point in the history
feat: Fork before cloning
  • Loading branch information
siketyan authored Mar 25, 2023
2 parents f04e9b5 + ce48ee6 commit 26404b4
Show file tree
Hide file tree
Showing 9 changed files with 1,013 additions and 22 deletions.
803 changes: 791 additions & 12 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ authors = [

[dependencies]
anyhow = "1.0"
async-hofs = "0.1.1"
async-trait = "0.1.67"
build-info = "0.0.30"
clap = { version = "4.1", features = ["derive"] }
console = "0.15.2"
Expand All @@ -28,15 +30,20 @@ serde = { version = "1.0", features = ["derive"] }
serde_regex = "1.1"
serde_with = "2.3"
tokio = { version = "1.26", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1.12"
toml = "0.7.2"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
url = "2.3"
walkdir = "2.3"

gh-config = { version = "0.2.1", optional = true }
octocrab = { version = "0.18.1", optional = true }

[build-dependencies]
build-info-build = "0.0.30"

[features]
default = []
default = ["github"]
vendored = ["git2/vendored-libgit2", "git2/vendored-openssl"]
github = ["gh-config", "octocrab"]
10 changes: 10 additions & 0 deletions ghr.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ owner = "siketyan"
# 'Cli' is the default and only supported.
strategy.clone = "Cli"

[platforms.github]
# Default configuration for GitHub.com.
type = "github"

[platforms.ghe-acme]
# If you are using a GitHub Enterprise Server instance,
# Specify here to enable `--fork` working with repositories on the GHE server.
type = "github"
host = "ghe.example.com"

[[patterns]]
# You can use additional patterns to specify where the repository is cloned from.
# For details of regular expression syntax, see https://docs.rs/regex/latest/regex/index.html .
Expand Down
50 changes: 42 additions & 8 deletions src/cmd/clone.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::path::PathBuf;

use anyhow::Result;
use anyhow::{anyhow, Result};
use async_hofs::iter::AsyncMapExt;
use clap::Parser;
use console::style;
use git2::Repository;
use itertools::Itertools;
use tokio_stream::StreamExt;
use tracing::info;

use crate::config::Config;
Expand All @@ -19,6 +21,10 @@ pub struct Cmd {
/// URL or pattern of the repository to clone.
repo: Vec<String>,

/// Forks the repository in the specified owner (organisation) and clones the forked repo.
#[clap(long)]
fork: Option<Option<String>>,

/// Clones their submodules recursively.
#[clap(short, long)]
recursive: bool,
Expand All @@ -37,11 +43,20 @@ impl Cmd {
let root = Root::find()?;
let config = Config::load_from(&root)?;

let urls = self
.repo
.iter()
.async_map(|repo| self.url(&config, repo))
.collect::<Result<Vec<_>>>()
.await?;

let repo: Vec<CloneResult> = Spinner::new("Cloning the repository...")
.spin_while(|| async move {
self.repo
.iter()
.map(|repo| self.clone(&root, &config, repo))
urls.into_iter()
.map(|url| {
info!("Cloning from '{}'", url.to_string());
self.clone(&root, &config, url)
})
.try_collect()
})
.await?;
Expand Down Expand Up @@ -76,16 +91,35 @@ impl Cmd {
Ok(())
}

fn clone(&self, root: &Root, config: &Config, repo: &str) -> Result<CloneResult> {
let url = Url::from_str(repo, &config.patterns, config.defaults.owner.as_deref())?;
async fn url(&self, config: &Config, repo: &str) -> Result<Url> {
let mut url = Url::from_str(repo, &config.patterns, config.defaults.owner.as_deref())?;

if let Some(owner) = &self.fork {
info!("Forking from '{}'", url.to_string());

let platform = config
.platforms
.find(&url)
.ok_or_else(|| anyhow!("Could not find a platform to fork on."))?
.try_into_platform()?;

url = Url::from_str(
&platform.fork(&url, owner.clone()).await?,
&config.patterns,
config.defaults.owner.as_deref(),
)?;
}

Ok(url)
}

fn clone(&self, root: &Root, config: &Config, url: Url) -> Result<CloneResult> {
let path = PathBuf::from(Path::resolve(root, &url));
let profile = config
.rules
.resolve(&url)
.and_then(|r| config.profiles.resolve(&r.profile));

info!("Cloning from '{}'", url.to_string());

config.git.strategy.clone.clone_repository(
url,
&path,
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde::Deserialize;

use crate::application::Applications;
use crate::git::Config as GitConfig;
use crate::platform::Config as PlatformConfig;
use crate::profile::Profiles;
use crate::root::Root;
use crate::rule::Rules;
Expand All @@ -23,6 +24,8 @@ pub struct Config {
#[serde(default)]
pub git: GitConfig,
#[serde(default)]
pub platforms: PlatformConfig,
#[serde(default)]
pub patterns: Patterns,
#[serde(default)]
pub profiles: Profiles,
Expand Down
2 changes: 1 addition & 1 deletion src/git/strategy/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl CloneRepository for Cli {
true => Ok(()),
_ => Err(anyhow!(
"Error occurred while cloning the repository: {}",
String::from_utf8_lossy(output.stderr.as_slice()),
String::from_utf8_lossy(output.stderr.as_slice()).trim(),
)),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod config;
mod console;
mod git;
mod path;
mod platform;
mod profile;
mod repository;
mod root;
Expand Down
76 changes: 76 additions & 0 deletions src/platform/github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use gh_config::{Hosts, GITHUB_COM};
use octocrab::Octocrab;
use serde::Deserialize;

use crate::platform::{Fork, Platform, PlatformInit};
use crate::url::Url;

fn default_host() -> String {
GITHUB_COM.to_string()
}

#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(default = "default_host")]
pub(super) host: String,
}

impl Default for Config {
fn default() -> Self {
Self {
host: default_host(),
}
}
}

pub struct GitHub {
client: Octocrab,
}

impl PlatformInit for GitHub {
type Config = Config;

fn init(config: &Config) -> Result<Self> {
let token = Hosts::load()?
.get(&config.host)
.ok_or_else(|| {
anyhow!(
"gh CLI does not have any token for github.com. Run `gh auth login` and retry."
)
})?
.oauth_token
.clone();

let mut builder = Octocrab::builder().personal_token(token);
if config.host != GITHUB_COM {
builder = builder.base_url(format!("https://{}/api/v3", &config.host))?;
}

Ok(Self {
client: builder.build()?,
})
}
}

impl Platform for GitHub {}

#[async_trait]
impl Fork for GitHub {
async fn fork(&self, url: &Url, owner: Option<String>) -> Result<String> {
let request = self.client.repos(&url.owner, &url.repo);
let request = match owner {
Some(o) => request.create_fork().organization(o),
_ => request.create_fork(),
};

Ok(request
.send()
.await?
.html_url
.as_ref()
.ok_or_else(|| anyhow!("GitHub API did not return HTML URL for the repository."))?
.to_string())
}
}
81 changes: 81 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
mod github;

use std::result::Result as StdResult;

use anyhow::Result;
use async_trait::async_trait;
use serde::Deserialize;
use std::collections::HashMap;

use crate::url::Url;

#[async_trait]
pub trait Fork {
async fn fork(&self, url: &Url, owner: Option<String>) -> Result<String>;
}

pub trait PlatformInit: Sized {
type Config;

fn init(config: &Self::Config) -> Result<Self>;
}

pub trait Platform: Fork {}

#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
pub enum PlatformConfig {
#[cfg(feature = "github")]
#[serde(rename = "github")]
GitHub(github::Config),
}

impl PlatformConfig {
pub fn try_into_platform(&self) -> Result<Box<dyn Platform>> {
self.try_into()
}

fn host(&self) -> String {
match self {
#[cfg(feature = "github")]
Self::GitHub(c) => c.host.to_string(),
}
}
}

impl TryInto<Box<dyn Platform>> for &PlatformConfig {
type Error = anyhow::Error;

fn try_into(self) -> StdResult<Box<dyn Platform>, Self::Error> {
Ok(match self {
#[cfg(feature = "github")]
PlatformConfig::GitHub(c) => Box::new(github::GitHub::init(c)?),
})
}
}

#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(flatten)]
map: HashMap<String, PlatformConfig>,
}

impl Default for Config {
fn default() -> Self {
Self {
map: HashMap::from([
#[cfg(feature = "github")]
(
"github".to_string(),
PlatformConfig::GitHub(github::Config::default()),
),
]),
}
}
}

impl Config {
pub fn find(&self, url: &Url) -> Option<&PlatformConfig> {
self.map.values().find(|c| c.host() == url.host.to_string())
}
}

0 comments on commit 26404b4

Please sign in to comment.