use arboard::Clipboard;
use clap::Parser;
use color_eyre::Result;
use giti::{
cli::{Cli, Commands, InfoArgs, OpenArgs},
repo::Repo,
};
use linkify::{LinkFinder, LinkKind};
use std::io::{self, BufRead, IsTerminal};
use std::path::PathBuf;
use std::process::Command;
use url::{ParseError, Url};
use yansi::Paint;
fn main() {
let cli = Cli::parse();
match cli.command {
Some(Commands::Clone { url }) => {
if let Err(e) = handle_clone(url) {
eprintln!("{}", format!("Clone failed: {e}").red());
std::process::exit(1);
}
}
Some(Commands::Info(mut args)) => {
handle_info(&mut args);
}
Some(Commands::Open(args)) => {
if let Err(e) = handle_open(&args) {
eprintln!("{}", format!("Open failed: {e}").red());
std::process::exit(1);
}
}
None => {
let mut args = InfoArgs::default();
handle_info(&mut args);
}
}
}
fn handle_info(args: &mut InfoArgs) {
args.setup_defaults();
let paths = if args.paths.is_empty() {
vec![PathBuf::from(".")]
} else {
args.paths.clone()
};
let repos: Vec<Result<Repo>> = paths.iter().map(Repo::new).collect();
for repo in repos {
match repo {
Ok(r) => {
if args.tsv {
r.print_tsv_info(args);
} else {
r.print_human_readable_info(args);
}
}
Err(e) => eprintln!("{}", format!("{e}").red()),
}
}
}
fn handle_open(args: &OpenArgs) -> Result<()> {
let path = args.path.clone().unwrap_or_else(|| PathBuf::from("."));
let repo = Repo::new(&path)?;
let url = repo.get_web_url(&args.remote, &args.page, args.forge.as_deref())?;
println!("Opening: {}", url);
#[cfg(target_os = "macos")]
{
Command::new("open").arg(&url).status()?;
}
#[cfg(target_os = "linux")]
{
Command::new("xdg-open").arg(&url).status()?;
}
#[cfg(target_os = "windows")]
{
Command::new("cmd").args(["/C", "start", &url]).status()?;
}
Ok(())
}
fn handle_clone(url: Option<String>) -> Result<()> {
let input_text = get_input_text(url)?;
let extracted_urls = extract_urls(&input_text);
eprintln!("Found {} urls in input.", extracted_urls.len());
for url in extracted_urls {
match url {
Ok(repo_url) => {
eprintln!("Cloning url: {}...", repo_url);
clone_repo(repo_url)?
}
Err(e) => eprintln!("Error: {}", e),
}
}
Ok(())
}
fn clone_repo(repo_url: Url) -> Result<()> {
let clean_repo_url = clean_git_url(&repo_url)?;
if !is_valid_git_url(&clean_repo_url.to_string()) {
return Err(color_eyre::eyre::eyre!(
"Invalid git URL: {}",
clean_repo_url
));
}
let home = std::env::var("HOME")
.map_err(|_| color_eyre::eyre::eyre!("HOME environment variable not set"))?;
let repos_dir = PathBuf::from(home).join("Repos");
if !repos_dir.exists() {
std::fs::create_dir_all(&repos_dir)?;
println!("Created directory: {}", repos_dir.display());
}
let repo_name = extract_repo_name(&clean_repo_url.to_string())?;
let dest_path = repos_dir.join(&repo_name);
if dest_path.exists() && dest_path.join(".git").exists() {
println!(
"Repository already exists at {}, pulling latest changes...",
dest_path.display()
);
let status = Command::new("git")
.arg("pull")
.current_dir(&dest_path)
.status()?;
if status.success() {
println!("{}", "Pull successful!".green());
Ok(())
} else {
Err(color_eyre::eyre::eyre!(
"git pull exited with status: {}",
status
))
}
} else {
println!("Cloning {} to {}", clean_repo_url, repos_dir.display());
let status = Command::new("git")
.arg("clone")
.arg(&clean_repo_url.to_string())
.current_dir(&repos_dir)
.status()?;
if status.success() {
println!("{}", "Clone successful!".green());
Ok(())
} else {
Err(color_eyre::eyre::eyre!(
"git clone exited with status: {}",
status
))
}
}
}
fn extract_repo_name(url: &str) -> Result<String> {
let url = url.trim_end_matches('/');
let url = url.strip_suffix(".git").unwrap_or(url);
let name = url
.rsplit('/')
.next()
.or_else(|| url.rsplit(':').next())
.ok_or_else(|| color_eyre::eyre::eyre!("Could not extract repository name from URL"))?;
if name.is_empty() {
return Err(color_eyre::eyre::eyre!(
"Could not extract repository name from URL"
));
}
Ok(name.to_string())
}
fn get_input_text(url: Option<String>) -> Result<String> {
if !io::stdin().is_terminal() {
let stdin = io::stdin();
if let Some(Ok(line)) = stdin.lock().lines().next() {
let trimmed = line.trim().to_string();
if !trimmed.is_empty() {
println!("Using STDIN source");
return Ok(trimmed);
}
}
}
if let Some(u) = url {
println!("Using Arg source");
return Ok(u);
}
let mut clipboard = Clipboard::new()?;
let text = clipboard.get_text()?;
let trimmed = text.trim().to_string();
println!("Using Clipboard source");
Ok(trimmed)
}
fn extract_urls(text: &str) -> Vec<Result<Url, ParseError>> {
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
finder.links(text).map(|l| Url::parse(l.as_str())).collect()
}
fn is_valid_git_url(url: &str) -> bool {
url.starts_with("https://")
|| url.starts_with("http://")
|| url.starts_with("git://")
|| url.starts_with("git@")
|| url.starts_with("ssh://")
|| url.ends_with(".git")
}
fn clean_git_url(url: &Url) -> Result<Url> {
let mut url = url.clone();
let segments = url
.path_segments()
.ok_or_else(|| color_eyre::eyre::eyre!("Cannot be a base URL"))?
.map(|s| s.to_owned());
let segments_count: usize = segments.count();
if segments_count > 2 {
let mut segs_mut = url
.path_segments_mut()
.map_err(|_| color_eyre::eyre::eyre!("Cannot be a base URL"))?;
for _ in 0..(segments_count - 2) {
segs_mut.pop();
}
}
url.set_query(None);
Ok(url)
}