Skip to content

Feature/add libra tag #775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
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
29 changes: 29 additions & 0 deletions aria/contents/docs/libra/command/config/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,32 @@
title: The [config] Command
description:
---

# config

The `config` command is used to get and set repository or global options, such as user information, aliases, and configuration settings.

### Usage

config [OPTIONS] [key] [value_pattern]

### Arguments

- `key` (optional): The key string of the configuration entry. Must follow the format `configuration.[name].key`. If the `list` mode is used, this argument is not required.
- `value_pattern` (optional): The value or a possible value pattern of the configuration entry. Required for certain modes like `add` or `set`.

### Description

Key parsing rules:
1. If the key does not contain a `.` symbol, an error is raised.
2. If the key contains one `.` symbol, it is parsed as `configuration.key`.
3. If the key contains more than one `.` symbol, it is parsed as `configuration.name.key`, where the first and last `.` are used to split the key.

### OPTIONS

- `--add`: Adds a new configuration entry to the database. Requires both `key` and `value_pattern`.
- `--get`: Retrieves a single configuration entry that matches the key and optionally the value pattern.
- `--get-all`: Retrieves all configuration entries that match the key and optionally the value pattern.
- `--unset`: Removes a single configuration entry that matches the key and optionally the value pattern.
- `--unset-all`: Removes all configuration entries that match the key and optionally the value pattern.
- `--list`: Lists all configuration entries in the database. Does not require `key` or `value_pattern`.
42 changes: 42 additions & 0 deletions aria/contents/docs/libra/command/tag/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,45 @@
title: The [tag] Command
description:
---

# tag

**Create, list, delete or verify a tag object**

### Usage

libra tag [OPTIONS] [TAG_NAME] [COMMIT_HASH]

### Arguments

- `[TAG_NAME]` - Name of the tag
- `[COMMIT_HASH]` - Base commit hash

### Description

If --list is given, or if there are no non-option arguments, existing tags are listed;
If a new tag name is given, a new tag is created that points to the given commit hash.
If no commit hash is given, the new tag will point to the current commit.

### OPTIONS

-t, --list
List all tags. This is the default behavior if no other options are specified.

-n, --new-tag <TAG_NAME>
Name of the tag to create. Use this option to create a new tag.

-a, --annotate <TAG_NAME>
Create a new annotated tag with the specified name.

-m, --message <MESSAGE>
The message for an annotated tag. This option requires the `--annotate` option.

-d, --delete <TAG_NAME>
Delete the specified tag.

-f, --force bool
overwirte current tag

<COMMIT_HASH>
Commit hash or tag name to tag. The default is HEAD. This option is required if `--new-tag` or `--annotate` is specified.
Binary file added libra.zip
Binary file not shown.
3 changes: 3 additions & 0 deletions libra/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ enum Commands {
Remote(command::remote::RemoteCmds),
#[command(about = "Manage repository configurations")]
Config(command::config::ConfigArgs),
#[command(about = "set tag for the current commit")]
Tag(command::tag::TagArgs),

// other hidden commands
#[command(
Expand Down Expand Up @@ -113,6 +115,7 @@ pub async fn parse_async(args: Option<&[&str]>) -> Result<(), GitError> {
Commands::Diff(args) => command::diff::execute(args).await,
Commands::Remote(cmd) => command::remote::execute(cmd).await,
Commands::Pull(args) => command::pull::execute(args).await,
Commands::Tag(args) => command::tag::execute(args).await,
Commands::Config(args) => command::config::execute(args).await,
}
Ok(())
Expand Down
1 change: 1 addition & 0 deletions libra/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod restore;
pub mod status;
pub mod switch;
pub mod config;
pub mod tag;

use crate::internal::branch::Branch;
use crate::internal::head::Head;
Expand Down
178 changes: 178 additions & 0 deletions libra/src/command/tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use crate::{
command::get_target_commit, internal::{tag::TagInfo, head::Head}, internal::config
};
use clap::Parser;
use mercury::internal::object::{commit::Commit, signature::SignatureType, tag::Tag, types::ObjectType};
use mercury::internal::object::signature::Signature;
use mercury::hash::SHA1;


use crate::command::load_object;
use super::save_object;

#[derive(Parser, Debug)]
pub struct TagArgs {
/// Name of the tag to create, delete, or inspect
#[clap(group = "create")]
tag_name: Option<String>,

/// Commit hash or tag name to tag (default is HEAD)
#[clap(requires = "create")]
commit_hash: Option<String>,

/// List all tags
#[clap(short, long, group = "sub", default_value = "true")]
list: bool,

/// Create a new tag (lightweight or annotated)
#[clap(short = 'a', long, group = "sub", group = "create")]
annotate: Option<String>,

/// The message for an annotated tag
#[clap(short = 'm', long, requires = "annotate")]
message: Option<String>,

/// Delete a tag
#[clap(short = 'd', long, group = "sub", requires = "tag_name")]
delete: bool,

/// change current tag
#[clap(short = 'f', long)]
force: bool,
}

pub async fn execute(args: TagArgs) {
if args.delete {
delete_tag(args.tag_name.unwrap()).await;
} else if args.tag_name.is_some() {
create_tag(args.tag_name.unwrap(), args.commit_hash, args.force).await;
} else if args.annotate.is_some() {
create_annotated_tag(args.annotate.unwrap(), args.message, args.commit_hash, args.force).await;
} else if args.list {
// default behavior
list_tags().await;
} else {
panic!("should not reach here")
Copy link
Preview

Copilot AI Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected cases should be handled more gracefully instead of panicking.

Suggested change
panic!("should not reach here")
eprintln!("Error: unexpected case encountered");

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

}
}

pub async fn create_tag(tag_name: String, commit_hash: Option<String>, force: bool){
tracing::debug!("create tag: {} from {:?}", tag_name, commit_hash);

if !is_valid_git_tag_name(&tag_name) {
eprintln!("fatal: invalid tag name: {}", tag_name);
Copy link
Preview

Copilot AI Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an error would be more appropriate than just printing and returning.

Suggested change
eprintln!("fatal: invalid tag name: {}", tag_name);
return Err(format!("fatal: invalid tag name: {}", tag_name));

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

return;
}

// check if tag exists
let tag = TagInfo::find_tag(&tag_name).await;
if tag.is_some() && !force {
panic!("fatal: A tag named '{}' already exists.", tag_name);
}

let commit_id = match commit_hash {
Some(commit_hash) => {
let commit = get_target_commit(&commit_hash).await;
match commit {
Ok(commit) => commit,
Err(e) => {
eprintln!("fatal: {}", e);
return;
}
}
}
None => Head::current_commit().await.unwrap(),
};
tracing::debug!("base commit_id: {}", commit_id);

// check if commit_hash exists
let _ = load_object::<Commit>(&commit_id)
.unwrap_or_else(|_| panic!("fatal: not a valid object name: '{}'", commit_id));

// create tag
TagInfo::update_tag(&tag_name, &commit_id.to_string()).await;
}


async fn create_annotated_tag(tag_name: String, message: Option<String>, commit_hash: Option<String>, force: bool) {
create_tag(tag_name.clone(), commit_hash.clone(), force).await;
let commit_id = match commit_hash {
Some(commit_hash) => {
let commit = get_target_commit(&commit_hash).await;
match commit {
Ok(commit) => commit,
Err(e) => {
eprintln!("fatal: {}", e);
return;
}
}
}
None => Head::current_commit().await.unwrap(),
};
let author = config::Config::get("user", None, "name").await.unwrap();
let email = config::Config::get("user", None, "email").await.unwrap();
let tag = Tag {
id: SHA1::default(),
object_hash: commit_id,
object_type: ObjectType::Tag,
tag_name,
tagger: Signature::new(SignatureType::Tagger, author.to_owned(), email.to_owned()),
message: message.unwrap_or_default()
};
save_object(&tag, &tag.id).unwrap();
}

async fn delete_tag(tag_name: String) {
let _ = TagInfo::find_tag(&tag_name)
.await
.unwrap_or_else(|| panic!("fatal: tag '{}' not found", tag_name));

TagInfo::delete_tag(&tag_name).await;
}

async fn list_tags() {
let tags = TagInfo::list_tags().await;
for tag in tags {
println!("{}", tag.name);
}
}



fn is_valid_git_tag_name(tag_name: &str) -> bool {
// Check for empty or invalid length
if tag_name.is_empty() || tag_name.len() > 255 {
return false;
}

// Reserved names
let reserved_names = [
"HEAD", "@", "FETCH_HEAD", "ORIG_HEAD", "MERGE_HEAD", "REBASE_HEAD",
];
if reserved_names.contains(&tag_name) {
return false;
}

// Check for forbidden characters
let forbidden_chars = [' ', '~', '^', ':', '?', '*', '[', '\x00', '\x7f'];
if tag_name.chars().any(|c| forbidden_chars.contains(&c) || c.is_control()) {
return false;
}

// Check for invalid start or end characters
if tag_name.starts_with('.') || tag_name.starts_with('/') || tag_name.ends_with('.') || tag_name.ends_with('/') {
return false;
}

// Check for double slashes
if tag_name.contains("//") {
return false;
}

// Check for trailing '@'
if tag_name.ends_with('@') {
return false;
}

true
}
1 change: 1 addition & 0 deletions libra/src/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod db;
pub mod head;
pub mod model;
pub mod protocol;
pub mod tag;
99 changes: 99 additions & 0 deletions libra/src/internal/tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::str::FromStr;

use sea_orm::ActiveModelTrait;
use sea_orm::ActiveValue::Set;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};

use mercury::hash::SHA1;

use crate::internal::db::get_db_conn_instance;
use crate::internal::model::reference;

pub struct TagInfo {
pub name: String,
pub commit: SHA1,
}

impl TagInfo {

pub async fn query_reference(tag_name: &str) -> Option<reference::Model> {
let db_conn = get_db_conn_instance().await;
reference::Entity::find()
.filter(reference::Column::Name.eq(tag_name))
.filter(reference::Column::Kind.eq(reference::ConfigKind::Tag))
.one(db_conn)
.await
.unwrap()
Copy link
Preview

Copilot AI Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using unwrap() can cause a panic if the result is None. Consider handling the None case more gracefully.

Suggested change
.unwrap()
.await.ok()?

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

}

/// list all tags
pub async fn list_tags() -> Vec<TagInfo> {
let db_conn = get_db_conn_instance().await;
let tags = reference::Entity::find()
.filter(reference::Column::Kind.eq(reference::ConfigKind::Tag))
.all(db_conn)
.await
.unwrap();

tags
.iter()
.map(|tag| TagInfo {
name: tag.name.as_ref().unwrap().clone(),
commit: SHA1::from_str(tag.commit.as_ref().unwrap()).unwrap(),
})
.collect()
}

/// is the tag exists
Copy link
Preview

Copilot AI Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment should be 'Check if the tag exists'.

Suggested change
/// is the tag exists
/// Check if the tag exists

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

pub async fn exists(tag_name: &str) -> bool {
let tag = Self::find_tag(tag_name).await;
tag.is_some()
}

/// get the tag by name
pub async fn find_tag(tag_name: &str) -> Option<TagInfo> {
let tag = Self::query_reference(tag_name).await;
match tag {
Some(tag) => Some(TagInfo {
name: tag.name.as_ref().unwrap().clone(),
commit: SHA1::from_str(tag.commit.as_ref().unwrap()).unwrap(),
Copy link
Preview

Copilot AI Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using unwrap() can cause a panic if the result is None. Consider handling the None case more gracefully.

Suggested change
commit: SHA1::from_str(tag.commit.as_ref().unwrap()).unwrap(),
commit: tag.commit.as_ref().and_then(|c| SHA1::from_str(c).ok()),

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

}),
None => None,
}
}

/// update the tag
pub async fn update_tag(tag_name: &str, commit_hash: &str) {
let db_conn = get_db_conn_instance().await;
// check if tag exists
let tag = Self::query_reference(tag_name).await;

match tag {
Some(tag) => {
let mut tag: reference::ActiveModel = tag.into();
tag.commit = Set(Some(commit_hash.to_owned()));
tag.update(db_conn).await.unwrap();
}
None => {
reference::ActiveModel {
name: Set(Some(tag_name.to_owned())),
kind: Set(reference::ConfigKind::Tag),
commit: Set(Some(commit_hash.to_owned())),
..Default::default()
}
.insert(db_conn)
.await
.unwrap();
}
}
}

pub async fn delete_tag(tag_name: &str) {
let db_conn = get_db_conn_instance().await;
let tag: reference::ActiveModel =
Self::query_reference(tag_name).await.unwrap().into();
tag.delete(db_conn).await.unwrap();
}


}
Loading