diff --git a/ceres/src/lfs/lfs_structs.rs b/ceres/src/lfs/lfs_structs.rs index 71ad4df80..4618dc345 100644 --- a/ceres/src/lfs/lfs_structs.rs +++ b/ceres/src/lfs/lfs_structs.rs @@ -23,13 +23,13 @@ pub struct MetaObject { pub struct RequestVars { pub oid: String, pub size: i64, - #[serde(default)] + #[serde(default, skip_serializing_if = "String::is_empty")] pub user: String, - #[serde(default)] + #[serde(default, skip_serializing_if = "String::is_empty")] pub password: String, - #[serde(default)] + #[serde(default, skip_serializing_if = "String::is_empty")] pub repo: String, - #[serde(default)] + #[serde(default, skip_serializing_if = "String::is_empty")] pub authorization: String, } @@ -107,6 +107,7 @@ pub struct FetchchunkResponse { #[derive(Serialize, Deserialize, Clone)] pub struct Link { pub href: String, + #[serde(default)] // Optional field pub header: HashMap, pub expires_at: String, } diff --git a/libra/Cargo.toml b/libra/Cargo.toml index af320fb1c..c59dd57d1 100644 --- a/libra/Cargo.toml +++ b/libra/Cargo.toml @@ -54,3 +54,4 @@ pager = "0.16.0" [dev-dependencies] tokio = { workspace = true, features = ["macros", "process"] } tracing-test = "0.2.4" +tempfile = { workspace = true } diff --git a/libra/src/cli.rs b/libra/src/cli.rs new file mode 100644 index 000000000..0be0a9e31 --- /dev/null +++ b/libra/src/cli.rs @@ -0,0 +1,122 @@ +//! This is the main entry point for the Libra. +//! It includes the definition of the CLI and the main function. +//! +//! +use clap::{Parser, Subcommand}; +use mercury::errors::GitError; +use crate::command; +use crate::utils; + +// The Cli struct represents the root of the command line interface. +#[derive(Parser, Debug)] +#[command(about = "Libra: A partial Git implemented in Rust", version = "0.1.0-pre")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +/// THe Commands enum represents the subcommands that can be used with the CLI. +/// subcommand's execute and args are defined in `command` module +#[derive(Subcommand, Debug)] +enum Commands { + // Each variant of the enum represents a subcommand. + // The about attribute provides a brief description of the subcommand. + // The arguments of the subcommand are defined in the command module. + + // Init and Clone are the only commands that can be executed without a repository + #[command(about = "Initialize a new repository")] + Init, + #[command(about = "Clone a repository into a new directory")] + Clone(command::clone::CloneArgs), + + // The rest of the commands require a repository to be present + #[command(about = "Add file contents to the index")] + Add(command::add::AddArgs), + #[command(about = "Remove files from the working tree and from the index")] + Rm(command::remove::RemoveArgs), + #[command(about = "Restore working tree files")] + Restore(command::restore::RestoreArgs), + #[command(about = "Show the working tree status")] + Status, + #[command(subcommand, about = "Large File Storage")] + Lfs(command::lfs::LfsCmds), + #[command(about = "Show commit logs")] + Log(command::log::LogArgs), + #[command(about = "List, create, or delete branches")] + Branch(command::branch::BranchArgs), + #[command(about = "Record changes to the repository")] + Commit(command::commit::CommitArgs), + #[command(about = "Switch branches")] + Switch(command::switch::SwitchArgs), + #[command(about = "Merge changes")] + Merge(command::merge::MergeArgs), + #[command(about = "Update remote refs along with associated objects")] + Push(command::push::PushArgs), + #[command(about = "Download objects and refs from another repository")] + Fetch(command::fetch::FetchArgs), + #[command(about = "Fetch from and integrate with another repository or a local branch")] + Pull(command::pull::PullArgs), + + #[command(subcommand, about = "Manage set of tracked repositories")] + Remote(command::remote::RemoteCmds), + + // other hidden commands + #[command( + about = "Build pack index file for an existing packed archive", + hide = true + )] + IndexPack(command::index_pack::IndexPackArgs), +} + +/// The main function is the entry point of the Libra application. +/// It parses the command-line arguments and executes the corresponding function. +/// - Caution: This is a `synchronous` function, it's declared as `async` to be able to use `[tokio::main]` +/// - `args`: parse from command line if it's `None`, otherwise parse from the given args +#[tokio::main] +pub async fn parse(args: Option<&[&str]>) -> Result<(), GitError> { + parse_async(args).await +} + +/// `async` version of the [parse] function +pub async fn parse_async(args: Option<&[&str]>) -> Result<(), GitError> { + let args = match args { + Some(args) => Cli::try_parse_from(args).map_err(|e| GitError::InvalidArgument(e.to_string()))?, + None => Cli::parse(), + }; + // TODO: try check repo before parsing + if let Commands::Init = args.command { + } else if let Commands::Clone(_) = args.command { + } else if !utils::util::check_repo_exist() { + return Err(GitError::RepoNotFound); + } + // parse the command and execute the corresponding function with it's args + match args.command { + Commands::Init => command::init::execute().await, + Commands::Clone(args) => command::clone::execute(args).await, + Commands::Add(args) => command::add::execute(args).await, + Commands::Rm(args) => command::remove::execute(args).unwrap(), + Commands::Restore(args) => command::restore::execute(args).await, + Commands::Status => command::status::execute().await, + Commands::Lfs(cmd) => command::lfs::execute(cmd).await, + Commands::Log(args) => command::log::execute(args).await, + Commands::Branch(args) => command::branch::execute(args).await, + Commands::Commit(args) => command::commit::execute(args).await, + Commands::Switch(args) => command::switch::execute(args).await, + Commands::Merge(args) => command::merge::execute(args).await, + Commands::Push(args) => command::push::execute(args).await, + Commands::IndexPack(args) => command::index_pack::execute(args), + Commands::Fetch(args) => command::fetch::execute(args).await, + Commands::Remote(cmd) => command::remote::execute(cmd).await, + Commands::Pull(args) => command::pull::execute(args).await, + } + Ok(()) +} + +/// this test is to verify that the CLI can be built without panicking +/// according [clap dock](https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_4/index.html) +#[test] +fn verify_cli() { + use clap::CommandFactory; + + Cli::command().debug_assert() +} diff --git a/libra/src/command/clone.rs b/libra/src/command/clone.rs index bd8f47989..2c3106002 100644 --- a/libra/src/command/clone.rs +++ b/libra/src/command/clone.rs @@ -78,7 +78,7 @@ pub async fn execute(args: CloneArgs) { name: "origin".to_string(), url: remote_repo.clone(), }; - fetch::fetch_repository(&remote_config).await; + fetch::fetch_repository(&remote_config, None).await; /* setup */ setup(remote_repo.clone()).await; diff --git a/libra/src/command/fetch.rs b/libra/src/command/fetch.rs index ea58d1a77..417e66cb9 100644 --- a/libra/src/command/fetch.rs +++ b/libra/src/command/fetch.rs @@ -24,13 +24,18 @@ use crate::{ }; use crate::utils::util; +const DEFAULT_REMOTE: &str = "origin"; + #[derive(Parser, Debug)] pub struct FetchArgs { - #[clap(long, short, group = "sub")] - repository: Option, + pub repository: Option, + + #[clap(requires("repository"))] + pub refspec: Option, - #[clap(long, short, group = "sub")] - all: bool, + /// Fetch all remotes. + #[clap(long, short, conflicts_with("repository"))] + pub all: bool, } pub async fn execute(args: FetchArgs) { @@ -39,27 +44,40 @@ pub async fn execute(args: FetchArgs) { if args.all { let remotes = Config::all_remote_configs().await; let tasks = remotes.into_iter().map(|remote| async move { - fetch_repository(&remote).await; + fetch_repository(&remote, None).await; }); futures::future::join_all(tasks).await; } else { let remote = match args.repository { Some(remote) => remote, - None => "origin".to_string(), // todo: get default remote + None => Config::get_current_remote().await.unwrap_or_else(|_| { + eprintln!("fatal: HEAD is detached"); + Some(DEFAULT_REMOTE.to_owned()) + }).unwrap_or_else(|| { + eprintln!("fatal: No remote configured for current branch"); + DEFAULT_REMOTE.to_owned() + }), }; let remote_config = Config::remote_config(&remote).await; match remote_config { - Some(remote_config) => fetch_repository(&remote_config).await, + Some(remote_config) => fetch_repository(&remote_config, args.refspec).await, None => { tracing::error!("remote config '{}' not found", remote); - eprintln!("fatal: '{}' does not appear to be a git repository", remote); + eprintln!("fatal: '{}' does not appear to be a libra repository", remote); } } } } -pub async fn fetch_repository(remote_config: &RemoteConfig) { - println!("fetching from {}", remote_config.name); +/// Fetch from remote repository +/// - `branch` is optional, if `None`, fetch all branches +pub async fn fetch_repository(remote_config: &RemoteConfig, branch: Option) { + println!("fetching from {}{}", remote_config.name, + if let Some(branch) = &branch { + format!(" ({})", branch) + } else { + "".to_owned() + }); // fetch remote let url = match Url::parse(&remote_config.url) { @@ -90,12 +108,29 @@ pub async fn fetch_repository(remote_config: &RemoteConfig) { return; } - let want = refs - .iter() + let remote_head = refs.iter().find(|r| r._ref == "HEAD").cloned(); + // remote branches + let mut ref_heads = refs // DO NOT use `refs` later + .into_iter() .filter(|r| r._ref.starts_with("refs/heads")) + .collect::>(); + + // filter by branch + if let Some(ref branch) = branch { + let branch = format!("refs/heads/{}", branch); + ref_heads.retain(|r| r._ref == branch); + + if ref_heads.is_empty() { + eprintln!("fatal: '{}' not found in remote", branch); + return; + } + } + + let want = ref_heads + .iter() .map(|r| r._hash.clone()) - .collect(); - let have = current_have().await; + .collect::>(); + let have = current_have().await; // TODO: return `DiscRef` rather than only hash, to compare `have` & `want` more accurately let mut result_stream = http_client .fetch_objects(&have, &want, auth.to_owned()) @@ -156,42 +191,53 @@ pub async fn fetch_repository(remote_config: &RemoteConfig) { let checksum = checksum.to_plain_str(); println!("checksum: {}", checksum); - let pack_file = utils::path::objects() - .join("pack") - .join(format!("pack-{}.pack", checksum)); - let mut file = fs::File::create(pack_file.clone()).unwrap(); - file.write_all(&pack_data).expect("write failed"); + if pack_data.len() > 32 { // 12 header + 20 hash + let pack_file = utils::path::objects() + .join("pack") + .join(format!("pack-{}.pack", checksum)); + let mut file = fs::File::create(pack_file.clone()).unwrap(); + file.write_all(&pack_data).expect("write failed"); - pack_file.to_string_or_panic() + Some(pack_file.to_string_or_panic()) + } else { + tracing::debug!("Empty pack file"); + None + } }; - /* build .idx file from PACK */ - index_pack::execute(IndexPackArgs { - pack_file, - index_file: None, - index_version: None, - }); + if let Some(pack_file) = pack_file { + /* build .idx file from PACK */ + index_pack::execute(IndexPackArgs { + pack_file, + index_file: None, + index_version: None, + }); + } /* update reference */ - for reference in refs.iter().filter(|r| r._ref.starts_with("refs/heads")) { - let branch_name = reference._ref.replace("refs/heads/", ""); + for r in &ref_heads { + let branch_name = r._ref.strip_prefix("refs/heads/").unwrap(); let remote = Some(remote_config.name.as_str()); - Branch::update_branch(&branch_name, &reference._hash, remote).await; + Branch::update_branch(branch_name, &r._hash, remote).await; } - let remote_head = refs.iter().find(|r| r._ref == "HEAD"); match remote_head { Some(remote_head) => { - let remote_head_name = refs + let remote_head_ref = ref_heads .iter() - .find(|r| r._ref.starts_with("refs/heads") && r._hash == remote_head._hash); + .find(|r| r._hash == remote_head._hash); - match remote_head_name { - Some(remote_head_name) => { - let remote_head_name = remote_head_name._ref.replace("refs/heads/", ""); - Head::update(Head::Branch(remote_head_name), Some(&remote_config.name)).await; + match remote_head_ref { + Some(remote_head_ref) => { + let remote_head_branch = remote_head_ref._ref.strip_prefix("refs/heads/").unwrap(); + Head::update(Head::Branch(remote_head_branch.to_owned()), Some(&remote_config.name)).await; } None => { - panic!("remote HEAD not found") + if branch.is_none() { + eprintln!("remote HEAD not found"); + } else { + // normal: remote HEAD usually points to master + tracing::debug!("Specified branch not found in remote HEAD"); + } } } } diff --git a/libra/src/command/pull.rs b/libra/src/command/pull.rs index a8294b23e..8c60c30f4 100644 --- a/libra/src/command/pull.rs +++ b/libra/src/command/pull.rs @@ -3,12 +3,19 @@ use crate::internal::{config::Config, head::Head}; use super::{fetch, merge}; use clap::Parser; #[derive(Parser, Debug)] -pub struct PullArgs; +pub struct PullArgs { + repository: Option, + + #[clap(requires("repository"))] + refspec: Option, +} pub async fn execute(args: PullArgs) { - let _ = args; - let fetch_args = fetch::FetchArgs::parse_from(Vec::::new()); - fetch::execute(fetch_args).await; + fetch::execute(fetch::FetchArgs { + repository: args.repository, + refspec: args.refspec, + all: false, + }).await; let head = Head::current().await; match head { diff --git a/libra/src/internal/config.rs b/libra/src/internal/config.rs index a7b3a128d..c9aabf92a 100644 --- a/libra/src/internal/config.rs +++ b/libra/src/internal/config.rs @@ -63,6 +63,7 @@ impl Config { } /// Get remote repo name of current branch + /// - `Error` if `HEAD` is detached pub async fn get_current_remote() -> Result, ()> { match Head::current().await { Head::Branch(name) => { diff --git a/libra/src/internal/protocol/https_client.rs b/libra/src/internal/protocol/https_client.rs index 6ac1acaa4..30b47758c 100644 --- a/libra/src/internal/protocol/https_client.rs +++ b/libra/src/internal/protocol/https_client.rs @@ -53,8 +53,8 @@ type DiscRef = DiscoveredReference; // protocol details: https://www.git-scm.com/docs/http-protocol // capability declarations: https://www.git-scm.com/docs/protocol-capabilities impl HttpsClient { - /// GET $GIT_URL/info/refs?service=git-upload-pack HTTP/1.0 - /// discover the references of the remote repository before fetching the objects. + /// GET $GIT_URL/info/refs?service=git-upload-pack HTTP/1.0
+ /// Discover the references of the remote repository before fetching the objects. /// the first ref named HEAD as default ref. /// ## Args /// - auth: (username, password) @@ -155,10 +155,10 @@ impl HttpsClient { Ok(ref_list) } - /// POST $GIT_URL/git-upload-pack HTTP/1.0 - /// Fetch the objects from the remote repository, which is specified by `have` and `want`. + /// POST $GIT_URL/git-upload-pack HTTP/1.0
+ /// Fetch the objects from the remote repository, which is specified by `have` and `want`.
/// `have` is the list of objects' hashes that the client already has, and `want` is the list of objects that the client wants. - /// Obtain the `want` references from the `discovery_reference` method. + /// Obtain the `want` references from the `discovery_reference` method.
/// If the returned stream is empty, it may be due to incorrect refs or an incorrect format. // TODO support some necessary options pub async fn fetch_objects( diff --git a/libra/src/internal/protocol/lfs_client.rs b/libra/src/internal/protocol/lfs_client.rs index 5d6c65e30..f1e4ab98e 100644 --- a/libra/src/internal/protocol/lfs_client.rs +++ b/libra/src/internal/protocol/lfs_client.rs @@ -1,20 +1,20 @@ -use std::collections::HashSet; -use std::path::Path; +use crate::command; +use crate::internal::config::Config; +use crate::internal::protocol::https_client::BasicAuth; +use crate::internal::protocol::ProtocolClient; +use crate::utils::lfs; use async_static::async_static; +use ceres::lfs::lfs_structs::{BatchRequest, FetchchunkResponse, Link, LockList, LockListQuery, LockRequest, Ref, Representation, RequestVars, UnlockRequest, VerifiableLockList, VerifiableLockRequest}; use futures_util::StreamExt; +use mercury::internal::object::types::ObjectType; +use mercury::internal::pack::entry::Entry; use reqwest::{Client, StatusCode}; use ring::digest::{Context, SHA256}; use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::path::Path; use tokio::io::AsyncWriteExt; use url::Url; -use ceres::lfs::lfs_structs::{BatchRequest, FetchchunkResponse, Link, LockList, LockListQuery, LockRequest, Ref, Representation, RequestVars, UnlockRequest, VerifiableLockList, VerifiableLockRequest}; -use mercury::internal::object::types::ObjectType; -use mercury::internal::pack::entry::Entry; -use crate::command; -use crate::internal::config::Config; -use crate::internal::protocol::https_client::BasicAuth; -use crate::internal::protocol::ProtocolClient; -use crate::utils::lfs; async_static! { pub static ref LFS_CLIENT: LFSClient = LFSClient::new().await; @@ -37,14 +37,17 @@ struct LfsBatchResponse { impl ProtocolClient for LFSClient { /// Construct LFSClient from a given Repo URL. fn from_url(repo_url: &Url) -> Self { - let lfs_server = Url::parse(&lfs::generate_lfs_server_url(repo_url.to_string())).unwrap(); + // The trailing slash is MUST, or `join()` method will replace the last segment. + // like: Url("/info/lfs").join("objects/batch") => "/info/objects/batch" + let lfs_server = lfs::generate_lfs_server_url(repo_url.to_string()) + "/"; // IMPORTANT + let lfs_server = Url::parse(&lfs_server).unwrap(); let client = Client::builder() - .http1_only() - .default_headers(lfs::LFS_HEADERS.clone()) + .default_headers(lfs::LFS_HEADERS.clone()) // will be overwritten by `json()`, careful! .build() .unwrap(); Self { - batch_url: lfs_server.join("/objects/batch").unwrap(), + // Caution: DO NOT start with `/`, or path after domain will be replaced. + batch_url: lfs_server.join("objects/batch").unwrap(), lfs_url: lfs_server, client, } @@ -140,7 +143,10 @@ impl LFSClient { hash_algo: lfs::LFS_HASH_ALGO.to_string(), }; - let mut request = self.client.post(self.batch_url.clone()).json(&batch_request); + let mut request = self.client + .post(self.batch_url.clone()) + .json(&batch_request) + .headers(lfs::LFS_HEADERS.clone()); if let Some(auth) = auth { request = request.basic_auth(auth.username, Some(auth.password)); } @@ -173,7 +179,7 @@ impl LFSClient { } let link = upload_link.unwrap(); - let mut request = self.client.put(link.href.clone()); + let mut request = self.client.put(&link.href); for (k, v) in &link.header { request = request.header(k, v); } @@ -210,11 +216,15 @@ impl LFSClient { hash_algo: lfs::LFS_HASH_ALGO.to_string(), }; - let request = self.client.post(self.batch_url.clone()).json(&batch_request); + let request = self.client + .post(self.batch_url.clone()) + .json(&batch_request) + .headers(lfs::LFS_HEADERS.clone()); let response = request.send().await.unwrap(); - let resp = response.json::().await.unwrap(); - tracing::debug!("LFS download response:\n {:#?}", serde_json::to_value(&resp).unwrap()); + let text = response.text().await.unwrap(); + tracing::debug!("LFS download response:\n {:#?}", serde_json::from_str::(&text).unwrap()); + let resp = serde_json::from_str::(&text).unwrap(); let link = resp.objects[0].actions.as_ref().unwrap().get("download").unwrap(); @@ -253,7 +263,7 @@ impl LFSClient { let mut stream = response.bytes_stream(); - while let Some(chunk) = stream.next().await { + while let Some(chunk) = stream.next().await { // TODO: progress bar let chunk = chunk.unwrap(); file.write_all(&chunk).await.unwrap(); checksum.update(&chunk); @@ -272,11 +282,15 @@ impl LFSClient { /// Only for MonoRepo (mega) async fn fetch_chunk_links(&self, obj_link: &str) -> Result, ()> { - let request = self.client.get(obj_link.to_owned() + "/chunks"); + let mut url = Url::parse(obj_link).unwrap(); + let path = url.path().trim_end_matches('/'); + url.set_path(&(path.to_owned() + "/chunks")); // reserve query params (for GitHub link) + + let request = self.client.get(url); let resp = request.send().await.unwrap(); let code = resp.status(); - if code == StatusCode::NOT_FOUND { - tracing::info!("Remote LFS Server not support Chunks API"); + if code == StatusCode::NOT_FOUND || code == StatusCode::FORBIDDEN { // GitHub maybe return 403 + tracing::info!("Remote LFS Server not support Chunks API, or forbidden."); return Err(()); } else if !code.is_success() { tracing::debug!("fatal: LFS get chunk hrefs failed. Status: {}, Message: {}", code, resp.text().await.unwrap()); @@ -292,7 +306,7 @@ impl LFSClient { // LFS locks API impl LFSClient { pub async fn get_locks(&self, query: LockListQuery) -> LockList { - let url = self.lfs_url.join("/locks").unwrap(); + let url = self.lfs_url.join("locks").unwrap(); let mut request = self.client.get(url); request = request.query(&[ ("id", query.id), @@ -317,7 +331,7 @@ impl LFSClient { /// lock an LFS file /// - `refspec` is must in Mega Server, but optional in Git Doc pub async fn lock(&self, path: String, refspec: String, basic_auth: Option) -> StatusCode { - let url = self.lfs_url.join("/locks").unwrap(); + let url = self.lfs_url.join("locks").unwrap(); let mut request = self.client.post(url).json(&LockRequest { path, refs: Ref { name: refspec }, @@ -334,7 +348,7 @@ impl LFSClient { } pub async fn unlock(&self, id: String, refspec: String, force: bool, basic_auth: Option) -> StatusCode { - let url = self.lfs_url.join(&format!("/locks/{}/unlock", id)).unwrap(); + let url = self.lfs_url.join(&format!("locks/{}/unlock", id)).unwrap(); let mut request = self.client.post(url).json(&UnlockRequest { force: Some(force), refs: Ref { name: refspec }, @@ -354,7 +368,7 @@ impl LFSClient { pub async fn verify_locks(&self, query: VerifiableLockRequest, basic_auth: Option) -> (StatusCode, VerifiableLockList) { - let url = self.lfs_url.join("/locks/verify").unwrap(); + let url = self.lfs_url.join("locks/verify").unwrap(); let mut request = self.client.post(url).json(&query); if let Some(auth) = basic_auth { request = request.basic_auth(auth.username, Some(auth.password)); @@ -373,4 +387,42 @@ impl LFSClient { } (code, resp.json::().await.unwrap()) } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_request_vars() { + let vars = RequestVars { + oid: "123".to_string(), + size: 123, + ..Default::default() + }; + println!("{:?}", serde_json::to_string(&vars).unwrap()); + } + + #[tokio::test] + async fn test_github_batch() { + let batch_request = BatchRequest { + operation: "download".to_string(), + transfers: vec![lfs::LFS_TRANSFER_API.to_string()], + objects: vec![RequestVars { + oid: "01cb1483670f1c497412f25f9f8f7dde31a8fab0960291035af03939ae1dfa6b".to_string(), + size: 104103, + ..Default::default() + }], + hash_algo: lfs::LFS_HASH_ALGO.to_string(), + }; + let lfs_client = LFSClient::from_url(&Url::parse("https://github.com/web3infra-foundation/mega.git").unwrap()); + let request = lfs_client.client + .post(lfs_client.batch_url.clone()) + .json(&batch_request) + .headers(lfs::LFS_HEADERS.clone()); + println!("Request {:?}", request); + let response = request.send().await.unwrap(); + let text = response.text().await.unwrap(); + println!("Text {:?}", text); + let _resp = serde_json::from_str::(&text).unwrap(); + } } \ No newline at end of file diff --git a/libra/src/lib.rs b/libra/src/lib.rs new file mode 100644 index 000000000..e54f61ef1 --- /dev/null +++ b/libra/src/lib.rs @@ -0,0 +1,37 @@ +use mercury::errors::GitError; + +mod command; +mod internal; +mod utils; +pub mod cli; + +/// Execute the Libra command in `sync` way. +/// ### Caution +/// There is a tokio runtime inside. Ensure you are NOT in a tokio runtime which can't be nested. +/// ### Example +/// - `["init"]` +/// - `["add", "."]` +pub fn exec(mut args: Vec<&str>) -> Result<(), GitError> { + args.insert(0, env!("CARGO_PKG_NAME")); + cli::parse(Some(&args)) +} + +/// Execute the Libra command in `async` way. +/// - `async` version of the [exec] function +pub async fn exec_async(mut args: Vec<&str>) -> Result<(), GitError> { + args.insert(0, env!("CARGO_PKG_NAME")); + cli::parse_async(Some(&args)).await +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + use super::*; + + #[test] + fn test_libra_init() { + let tmp_dir = TempDir::new().unwrap(); + std::env::set_current_dir(tmp_dir.path()).unwrap(); + exec(vec!["init"]).unwrap(); + } +} \ No newline at end of file diff --git a/libra/src/main.rs b/libra/src/main.rs index 477bf1762..83518c1b7 100644 --- a/libra/src/main.rs +++ b/libra/src/main.rs @@ -1,86 +1,13 @@ //! This is the main entry point for the Libra. -//! It includes the definition of the CLI and the main function. -//! -//! -use clap::{Parser, Subcommand}; + +use mercury::errors::GitError; mod command; mod internal; mod utils; +mod cli; -// The Cli struct represents the root of the command line interface. -#[derive(Parser, Debug)] -#[command(about = "Libra: A partial Git implemented in Rust", version = "0.1.0-pre")] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -/// THe Commands enum represents the subcommands that can be used with the CLI. -/// subcommand's execute and args are defined in `command` module -#[derive(Subcommand, Debug)] -enum Commands { - // Each variant of the enum represents a subcommand. - // The about attribute provides a brief description of the subcommand. - // The arguments of the subcommand are defined in the command module. - - // Init and Clone are the only commands that can be executed without a repository - #[command(about = "Initialize a new repository")] - Init, - #[command(about = "Clone a repository into a new directory")] - Clone(command::clone::CloneArgs), - - // The rest of the commands require a repository to be present - #[command(about = "Add file contents to the index")] - Add(command::add::AddArgs), - #[command(about = "Remove files from the working tree and from the index")] - Rm(command::remove::RemoveArgs), - #[command(about = "Restore working tree files")] - Restore(command::restore::RestoreArgs), - #[command(about = "Show the working tree status")] - Status, - #[command(subcommand, about = "Large File Storage")] - Lfs(command::lfs::LfsCmds), - #[command(about = "Show commit logs")] - Log(command::log::LogArgs), - #[command(about = "List, create, or delete branches")] - Branch(command::branch::BranchArgs), - #[command(about = "Record changes to the repository")] - Commit(command::commit::CommitArgs), - #[command(about = "Switch branches")] - Switch(command::switch::SwitchArgs), - #[command(about = "Merge changes")] - Merge(command::merge::MergeArgs), - #[command(about = "Update remote refs along with associated objects")] - Push(command::push::PushArgs), - #[command(about = "Download objects and refs from another repository")] - Fetch(command::fetch::FetchArgs), - #[command(about = "Fetch from and integrate with another repository or a local branch")] - Pull(command::pull::PullArgs), - - #[command(subcommand, about = "Manage set of tracked repositories")] - Remote(command::remote::RemoteCmds), - - // other hidden commands - #[command( - about = "Build pack index file for an existing packed archive", - hide = true - )] - IndexPack(command::index_pack::IndexPackArgs), -} - -// The main function is the entry point of the Libra application. -// It parses the command-line arguments and executes the corresponding function. -#[tokio::main] -async fn main() { - let args = Cli::parse(); - // TODO: try check repo before parsing - if let Commands::Init = args.command { - } else if let Commands::Clone(_) = args.command { - } else if !utils::util::check_repo_exist() { - return; - } - +fn main() { #[cfg(debug_assertions)] { tracing::subscriber::set_global_default( @@ -90,33 +17,14 @@ async fn main() { ) .unwrap(); } - // parse the command and execute the corresponding function with it's args - match args.command { - Commands::Init => command::init::execute().await, - Commands::Clone(args) => command::clone::execute(args).await, - Commands::Add(args) => command::add::execute(args).await, - Commands::Rm(args) => command::remove::execute(args).unwrap(), - Commands::Restore(args) => command::restore::execute(args).await, - Commands::Status => command::status::execute().await, - Commands::Lfs(cmd) => command::lfs::execute(cmd).await, - Commands::Log(args) => command::log::execute(args).await, - Commands::Branch(args) => command::branch::execute(args).await, - Commands::Commit(args) => command::commit::execute(args).await, - Commands::Switch(args) => command::switch::execute(args).await, - Commands::Merge(args) => command::merge::execute(args).await, - Commands::Push(args) => command::push::execute(args).await, - Commands::IndexPack(args) => command::index_pack::execute(args), - Commands::Fetch(args) => command::fetch::execute(args).await, - Commands::Remote(cmd) => command::remote::execute(cmd).await, - Commands::Pull(args) => command::pull::execute(args).await, - } -} - -/// this test is to verify that the CLI can be built without panicking -/// according [clap dock](https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_4/index.html) -#[test] -fn verify_cli() { - use clap::CommandFactory; - Cli::command().debug_assert() -} + let res = cli::parse(None); + match res { + Ok(_) => {} + Err(e) => { + if !matches!(e, GitError::RepoNotFound) { + eprintln!("Error: {:?}", e); + } + } + } +} \ No newline at end of file diff --git a/libra/src/utils/lfs.rs b/libra/src/utils/lfs.rs index 0496cb5d3..ba5e03ce2 100644 --- a/libra/src/utils/lfs.rs +++ b/libra/src/utils/lfs.rs @@ -75,9 +75,7 @@ pub fn format_pointer_string(oid: &str, size: u64) -> String { /// [doc: server-discovery](https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md) /// - like `https://git-server.com/foo/bar.git/info/lfs` /// - support ssh & https & git@ format -#[deprecated(note = "It's for git, not monorepo")] -#[allow(dead_code)] -pub fn generate_git_lfs_server_url(mut url: String) -> String { +fn generate_git_lfs_server_url(mut url: String) -> String { if url.ends_with('/') { url.pop(); } @@ -99,7 +97,11 @@ pub fn generate_git_lfs_server_url(mut url: String) -> String { /// Generate Mono LFS Server Url from repo Url. /// - Just get domain with port -pub fn generate_lfs_server_url(url: String) -> String { +/// ### Example +/// https://github.com/git-lfs/git-lfs/blob/main/docs/api/locking.md -> https://github.com +/// +/// http://localhost:8000/xxx/yyy -> http://localhost:8000 +fn generate_mono_lfs_server_url(url: String) -> String { let url = Url::parse(&url).unwrap(); match url.port() { None => { @@ -111,6 +113,31 @@ pub fn generate_lfs_server_url(url: String) -> String { } } +/// Generate LFS Server Url from repo Url. +/// - Automatically detect git or mono repo by domain +/// - Caution: without trailing slash `/` +pub fn generate_lfs_server_url(url_str: String) -> String { + let url = Url::parse(&url_str); + if url.is_err() { + // maybe start with `git@` + return generate_git_lfs_server_url(url_str); + } + let url = url.unwrap(); + match url.domain() { + Some(domain) => { + if domain == "github.com" || domain == "gitee.com" { + generate_git_lfs_server_url(url_str) + } else { + generate_mono_lfs_server_url(url_str) + } + } + None => { + // IP address, like http://127.0.0.1:8000 + generate_mono_lfs_server_url(url_str) + } + } +} + /// Generate LFS cache path, in `.libra/lfs/objects` pub fn lfs_object_path(oid: &str) -> PathBuf { util::storage_path() @@ -272,12 +299,16 @@ mod tests { #[test] fn test_gen_mono_lfs_server_url() { - const LFS_SERVER_URL: &str = "https://github.com/web3infra-foundation/mega.git/info/lfs"; + const LFS_SERVER_URL: &str = "https://gitmono.com/web3infra-foundation/mega.git/info/lfs"; assert_eq!( generate_lfs_server_url(LFS_SERVER_URL.to_owned()), - "https://github.com" + "https://gitmono.com" ); const LOCAL_LFS_SERVER_URL: &str = "http://localhost:8000/xxx/yyy"; + assert_eq!( + Url::parse(LOCAL_LFS_SERVER_URL).unwrap().domain().unwrap(), + "localhost" + ); assert_eq!( generate_lfs_server_url(LOCAL_LFS_SERVER_URL.to_owned()), "http://localhost:8000" diff --git a/mega/tests/lfs_test.rs b/mega/tests/lfs_test.rs index 48b5ade82..8c0350826 100644 --- a/mega/tests/lfs_test.rs +++ b/mega/tests/lfs_test.rs @@ -85,6 +85,9 @@ fn run_mega_server(data_dir: &Path) -> Child { if !MEGA.exists() { panic!("mega binary not found in \"target/debug/\", skip lfs test"); } + if is_port_in_use(PORT) { + panic!("port {} is already in use", PORT); + } // env var can be shared between parent and child process env::set_var("MEGA_BASE_DIR", data_dir); diff --git a/mercury/src/errors.rs b/mercury/src/errors.rs index 647ea1355..cfb8d1c8a 100644 --- a/mercury/src/errors.rs +++ b/mercury/src/errors.rs @@ -45,6 +45,9 @@ pub enum GitError { #[error("The `{0}` is not a valid index header.")] InvalidIndexHeader(String), + #[error("Argument parse failed: {0}")] + InvalidArgument(String), + #[error("IO Error: {0}")] IOError(#[from] std::io::Error), @@ -78,6 +81,9 @@ pub enum GitError { #[error("Can't find specific object: {0}")] ObjectNotFound(String), + #[error("Repository not found")] + RepoNotFound, + #[error("UnAuthorized: {0}")] UnAuthorized(String),