Skip to content
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
9 changes: 5 additions & 4 deletions ceres/src/lfs/lfs_structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -107,6 +107,7 @@ pub struct FetchchunkResponse {
#[derive(Serialize, Deserialize, Clone)]
pub struct Link {
pub href: String,
#[serde(default)] // Optional field
pub header: HashMap<String, String>,
pub expires_at: String,
}
Expand Down
1 change: 1 addition & 0 deletions libra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ pager = "0.16.0"
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "process"] }
tracing-test = "0.2.4"
tempfile = { workspace = true }
122 changes: 122 additions & 0 deletions libra/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 1 addition & 1 deletion libra/src/command/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
120 changes: 83 additions & 37 deletions libra/src/command/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub repository: Option<String>,

#[clap(requires("repository"))]
pub refspec: Option<String>,

#[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) {
Expand All @@ -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<String>) {
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) {
Expand Down Expand Up @@ -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::<Vec<_>>();

// 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::<Vec<_>>();
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())
Expand Down Expand Up @@ -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");
}
}
}
}
Expand Down
15 changes: 11 additions & 4 deletions libra/src/command/pull.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

#[clap(requires("repository"))]
refspec: Option<String>,
}

pub async fn execute(args: PullArgs) {
let _ = args;
let fetch_args = fetch::FetchArgs::parse_from(Vec::<String>::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 {
Expand Down
1 change: 1 addition & 0 deletions libra/src/internal/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<String>, ()> {
match Head::current().await {
Head::Branch(name) => {
Expand Down
Loading