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
38 changes: 37 additions & 1 deletion aria/contents/docs/libra/command/tag/index.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
---
title: The [tag] Command
description:
description: Create, list, delete, or show tag objects
---

### Usage

libra tag [OPTIONS] [NAME]

### Arguments

- `[NAME]`
The name of the tag to create, show, or delete.

### Description

The `libra tag` command is used to manage tag objects. Tags can be used to mark specific points in history as being important.

If no arguments are provided, or if the `--list` option is used, it will list all existing tags.

- To show the details of an existing tag, provide the tag name: `libra tag <tag-name>`
- To create a new annotated tag, provide a name and a message: `libra tag <tag-name> -m "Your tag message"`
- To delete an existing tag, provide the tag name and the delete flag: `libra tag <tag-name> -d`

### Options

- `-l`, `--list`
List all tags.
- `-d`, `--delete`
Delete a tag.
- `-m`, `--message <MESSAGE>`
Create an annotated tag with the given message. If not provided, the command will show tag details instead of creating a new one.
- `-h`, `--help`
Print help

<Note type="note" title="Note">
All parameters should align with Git’s behavior as closely as possible, but
there may be some differences. Refs https://git-scm.com/docs/git-tag for
more information.
</Note>
22 changes: 7 additions & 15 deletions common/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,17 @@ pub fn format_commit_msg(msg: &str, gpg_sig: Option<&str>) -> String {

/// parse commit message
pub fn parse_commit_msg(msg_gpg: &str) -> (&str, Option<&str>) {
const SIG_PATTERN: &str = r"^(gpgsig -----BEGIN (?:PGP|SSH) SIGNATURE-----[\s\S]*?-----END (?:PGP|SSH) SIGNATURE-----)";
const SIG_PATTERN: &str = r"^gpgsig (-----BEGIN (?:PGP|SSH) SIGNATURE-----[\s\S]*?-----END (?:PGP|SSH) SIGNATURE-----)";
const GPGSIG_PREFIX_LEN: usize = 7; // length of "gpgsig "

let sig_regex = Regex::new(SIG_PATTERN).unwrap();
if let Some(caps) = sig_regex.captures(msg_gpg) {
// Check if the signature type matches.
assert_eq!(
caps.get(2).map(|m| m.as_str()),
caps.get(3).map(|m| m.as_str())
);
let sig_len = caps.get(1).map(|m| m.as_str().len()).unwrap();
let signature = &msg_gpg[..sig_len];

// Skip the leading '\n\n' (blank lines).
// Some commit messages may use '\n \n\n' or similar patterns.
// To handle such cases, remove all leading blank lines from the message.
let msg = &msg_gpg[sig_len..].trim_start();
let signature = caps.get(1).unwrap().as_str();

let msg = &msg_gpg[signature.len() + GPGSIG_PREFIX_LEN..].trim_start();
Comment thread
genedna marked this conversation as resolved.
(msg, Some(signature))
} else {
assert!(msg_gpg.starts_with('\n'), "commit message format error");
(&msg_gpg[1..], None)
(msg_gpg.trim_start(), None)
}
}

Expand Down
3 changes: 3 additions & 0 deletions libra/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ enum Commands {
Log(command::log::LogArgs),
#[command(about = "List, create, or delete branches")]
Branch(command::branch::BranchArgs),
#[command(about = "Create a new tag")]
Tag(command::tag::TagArgs),
#[command(about = "Record changes to the repository")]
Commit(command::commit::CommitArgs),
#[command(about = "Switch branches")]
Expand Down Expand Up @@ -156,6 +158,7 @@ pub async fn parse_async(args: Option<&[&str]>) -> Result<(), GitError> {
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::Tag(args) => command::tag::execute(args).await,
Commands::Commit(args) => command::commit::execute(args).await,
Commands::Switch(args) => command::switch::execute(args).await,
Commands::Rebase(args) => command::rebase::execute(args).await,
Expand Down
7 changes: 4 additions & 3 deletions libra/src/command/commit.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use super::save_object;
use std::process::Stdio;
use std::str::FromStr;
use std::{collections::HashSet, path::PathBuf};

use super::save_object;
const EMPTY_TREE_HASH: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";

use crate::command::load_object;
use crate::internal::branch::Branch;
use crate::internal::config::Config as UserConfig;
Expand Down Expand Up @@ -230,8 +232,7 @@ pub async fn create_tree(index: &Index, storage: &ClientStorage, current_root: P
let tree = {
// `from_tree_items` can't create empty tree, so use `from_bytes` instead
if tree_items.is_empty() {
// git create a no zero hash for empty tree, didn't know method. use default SHA1 temporarily
Tree::from_bytes(&[], SHA1::default()).unwrap()
Tree::from_bytes(&[], SHA1::from_str(EMPTY_TREE_HASH).unwrap()).unwrap()
} else {
Tree::from_tree_items(tree_items).unwrap()
}
Expand Down
35 changes: 12 additions & 23 deletions libra/src/command/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ use tokio::io::{AsyncRead, AsyncReadExt};
use tokio_util::io::StreamReader;
use url::Url;

use crate::command::{load_object, HEAD};
use crate::command::load_object;
use crate::internal::db::get_db_conn_instance;
use crate::internal::reflog;
use crate::internal::reflog::{zero_sha1, ReflogAction, ReflogContext, ReflogError};
use crate::internal::reflog::{zero_sha1, Reflog, ReflogAction, ReflogContext, ReflogError, HEAD};
use crate::utils::util;
use crate::{
command::index_pack::{self, IndexPackArgs},
Expand Down Expand Up @@ -114,7 +113,7 @@ pub async fn fetch_repository(remote_config: RemoteConfig, branch: Option<String
return;
}

let remote_head = refs.iter().find(|r| r._ref == "HEAD").cloned();
let remote_head = refs.iter().find(|r| r._ref == HEAD).cloned();
// remote branches
let ref_heads = refs
.clone()
Expand Down Expand Up @@ -235,42 +234,32 @@ pub async fn fetch_repository(remote_config: RemoteConfig, branch: Option<String

// Determine the full ref name (e.g., "refs/remotes/origin/main")
if let Some(branch_name) = r._ref.strip_prefix("refs/heads/") {
full_ref_name = branch_name.to_owned();
full_ref_name =
format!("refs/remotes/{}/{}", remote_config.name, branch_name);
} else if let Some(mr_name) = r._ref.strip_prefix("refs/mr/") {
// Handle merge requests if your system supports them
full_ref_name = format!("mr/{}", mr_name);
} else if r._ref == HEAD {
continue;
full_ref_name =
format!("refs/remotes/{}/mr/{}", remote_config.name, mr_name);
} else {
tracing::warn!("Unsupported ref type during fetch: {}", r._ref);
continue; // Skip unsupported ref types
}

// Get the old OID *before* updating the branch
let old_oid = Branch::find_branch_with_conn(
txn,
&full_ref_name,
Some(&remote_config.name),
)
.await
.map_or(zero_sha1().to_string(), |b| b.commit.to_string());
let old_oid = Branch::find_branch_with_conn(txn, &full_ref_name, None)
.await
.map_or(zero_sha1().to_string(), |b| b.commit.to_string());

// Update the branch pointer
Branch::update_branch_with_conn(
txn,
&full_ref_name,
&r._hash,
Some(&remote_config.name),
)
.await;
Branch::update_branch_with_conn(txn, &full_ref_name, &r._hash, None).await;

// Prepare and insert the reflog entry for this specific remote-tracking branch
let context = ReflogContext {
old_oid: old_oid.to_string(),
new_oid: r._hash.clone(),
action: ReflogAction::Fetch, // Using a simple Fetch action
};
reflog::Reflog::insert_single_entry(txn, &context, &full_ref_name).await?;
Reflog::insert_single_entry(txn, &context, &full_ref_name).await?;
}

// 2. Update the remote's HEAD pointer
Expand Down
36 changes: 19 additions & 17 deletions libra/src/command/init.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! This module implements the `init` command for the Libra CLI.
//!
//!
//! The `init` command creates a new Libra repository in the current directory or a specified directory.
//! It supports customizing the initial branch name with the `--initial-branch` parameter.
//!
use std::{
fs,
Expand All @@ -17,7 +18,9 @@ use crate::internal::db;
use crate::internal::model::{config, reference};
use crate::utils::util::{DATABASE, ROOT_DIR};

#[derive(Parser, Debug)]
const DEFAULT_BRANCH: &str = "master";

#[derive(Parser, Debug, Clone)]
pub struct InitArgs {
/// Create a bare repository
#[clap(long, required = false)]
Expand Down Expand Up @@ -46,7 +49,6 @@ pub async fn execute(args: InitArgs) {
Ok(_) => {}
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
Expand Down Expand Up @@ -229,14 +231,17 @@ pub async fn init(args: InitArgs) -> io::Result<()> {
conn = db::create_database(database.to_str().unwrap()).await?;
}

// Create config table
init_config(&conn).await.unwrap();
// Create config table with bare parameter consideration
init_config(&conn, args.bare).await.unwrap();

// Determine the initial branch name: use provided name or default to "main"
let initial_branch_name = args
.initial_branch
.unwrap_or_else(|| DEFAULT_BRANCH.to_owned());

// Create HEAD
reference::ActiveModel {
name: Set(Some(
args.initial_branch.unwrap_or_else(|| "master".to_owned()),
)),
name: Set(Some(initial_branch_name.clone())),
kind: Set(reference::ConfigKind::Head),
..Default::default() // all others are `NotSet`
}
Expand All @@ -247,8 +252,9 @@ pub async fn init(args: InitArgs) -> io::Result<()> {
// Set .libra as hidden
set_dir_hidden(root_dir.to_str().unwrap())?;
if !args.quiet {
let repo_type = if args.bare { "bare " } else { "" };
println!(
"Initializing empty Libra repository in {}",
"Initializing empty {repo_type}Libra repository in {} with initial branch '{initial_branch_name}'",
root_dir.display()
);
}
Expand All @@ -258,7 +264,7 @@ pub async fn init(args: InitArgs) -> io::Result<()> {

/// Initialize the configuration for the Libra repository
/// This function creates the necessary configuration entries in the database.
async fn init_config(conn: &DbConn) -> Result<(), DbErr> {
async fn init_config(conn: &DbConn, is_bare: bool) -> Result<(), DbErr> {
// Begin a new transaction
let txn = conn.begin().await?;

Expand All @@ -267,7 +273,7 @@ async fn init_config(conn: &DbConn) -> Result<(), DbErr> {
let entries = [
("repositoryformatversion", "0"),
("filemode", "true"),
("bare", "false"),
("bare", if is_bare { "true" } else { "false" }),
("logallrefupdates", "true"),
];

Expand All @@ -276,7 +282,7 @@ async fn init_config(conn: &DbConn) -> Result<(), DbErr> {
let entries = [
("repositoryformatversion", "0"),
("filemode", "false"), // no filemode on windows
("bare", "false"),
("bare", if is_bare { "true" } else { "false" }),
("logallrefupdates", "true"),
("symlinks", "false"), // no symlinks on windows
("ignorecase", "true"), // ignorecase on windows
Expand All @@ -303,7 +309,7 @@ async fn init_config(conn: &DbConn) -> Result<(), DbErr> {
#[cfg(target_os = "windows")]
fn set_dir_hidden(dir: &str) -> io::Result<()> {
use std::process::Command;
Command::new("attrib").arg("+H").arg(dir).spawn()?.wait()?; // 等待命令执行完成
Command::new("attrib").arg("+H").arg(dir).spawn()?.wait()?; // Wait for command execution to complete
Ok(())
}

Expand All @@ -314,7 +320,3 @@ fn set_dir_hidden(_dir: &str) -> io::Result<()> {
// on unix-like systems, dotfiles are hidden by default
Ok(())
}

/// Unit tests for the init module
#[cfg(test)]
mod tests {}
38 changes: 10 additions & 28 deletions libra/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ pub mod remove;
pub mod reset;
pub mod restore;
pub mod revert;
pub mod tag;

pub mod stash;
pub mod status;
pub mod switch;

use crate::internal::branch::Branch;
use crate::internal::head::Head;
use crate::internal::protocol::https_client::BasicAuth;
use crate::utils;
use crate::utils::object_ext::BlobExt;
Expand All @@ -38,7 +38,7 @@ use std::io;
use std::io::Write;
use std::path::Path;

const HEAD: &str = "HEAD";


// impl load for all objects
pub fn load_object<T>(hash: &SHA1) -> Result<T, GitError>
Expand Down Expand Up @@ -108,29 +108,9 @@ pub fn calc_file_blob_hash(path: impl AsRef<Path>) -> io::Result<SHA1> {

/// Get the commit hash from branch name or commit hash, support remote branch
pub async fn get_target_commit(branch_or_commit: &str) -> Result<SHA1, Box<dyn std::error::Error>> {
if branch_or_commit == HEAD {
return Ok(Head::current_commit().await.unwrap());
}

let possible_branches = Branch::search_branch(branch_or_commit).await;
if possible_branches.len() > 1 {
return Err("Ambiguous branch name".into());
// TODO: git have a priority list of branches to use, continue with ambiguity, we didn't implement it yet
}

if possible_branches.is_empty() {
let storage = util::objects_storage();
let possible_commits = storage.search(branch_or_commit).await;
if possible_commits.len() > 1 {
return Err(format!("Ambiguous commit hash '{branch_or_commit}'").into());
}
if possible_commits.is_empty() {
return Err(format!("No such branch or commit: '{branch_or_commit}'").into());
}
Ok(possible_commits[0])
} else {
Ok(possible_branches[0].commit)
}
util::get_commit_base(branch_or_commit)
.await
.map_err(|e| e.into())
}

#[cfg(test)]
Expand Down Expand Up @@ -166,12 +146,14 @@ mod tests {
"gpgsig -----BEGIN SSH SIGNATURE-----\ncontent1\n-----END SSH SIGNATURE-----";
let msg_gpg = format_commit_msg(msg, Some(gpg_sig));
let msg_ssh = format_commit_msg(msg, Some(ssh_sig));
let gpg_sig_val = &gpg_sig[7..];
let ssh_sig_val = &ssh_sig[7..];
let (msg_, gpg_sig_) = parse_commit_msg(&msg_gpg);
let (msg__, ssh_sig__) = parse_commit_msg(&msg_ssh);
assert_eq!(msg, msg_);
assert_eq!(msg, msg__);
assert_eq!(gpg_sig, gpg_sig_.unwrap());
assert_eq!(ssh_sig, ssh_sig__.unwrap());
assert_eq!(gpg_sig_val, gpg_sig_.unwrap());
assert_eq!(ssh_sig_val, ssh_sig__.unwrap());

let msg_none = format_commit_msg(msg, None);
let (msg_, sig_) = parse_commit_msg(&msg_none);
Expand Down
10 changes: 1 addition & 9 deletions libra/src/command/rebase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,7 @@ pub async fn execute(args: RebaseArgs) {
/// then falls back to resolving it as a commit reference (hash, HEAD, etc.).
/// This allows the rebase command to work with both branch names and commit hashes.
async fn resolve_branch_or_commit(reference: &str) -> Result<SHA1, String> {
// First try to resolve as a branch name
if let Some(branch) = Branch::find_branch(reference, None).await {
return Ok(branch.commit);
}
// Fall back to commit hash resolution
match util::get_commit_base(reference).await {
Ok(id) => Ok(id),
Err(_) => Err(format!("invalid reference: {reference}")),
}
util::get_commit_base(reference).await
}

/// Replay a single commit on top of a new parent commit
Expand Down
Loading
Loading