|
1 | | -use crate::domain::{RepositoryInfo, RepoStatus, StatusItem, BranchInfo, CommitInfo, LocalBranch}; |
| 1 | +use crate::domain::{RepositoryInfo, RepoStatus, StatusItem, BranchInfo, CommitInfo, LocalBranch, TagInfo}; |
2 | 2 | use crate::error::{AppError, Result}; |
3 | 3 | use git2::{Repository, StatusOptions}; |
4 | 4 | use ignore::WalkBuilder; |
@@ -917,3 +917,206 @@ fn pull_branch_impl( |
917 | 917 |
|
918 | 918 | Ok(()) |
919 | 919 | } |
| 920 | + |
| 921 | +/// Get all tags for a repository |
| 922 | +#[tauri::command] |
| 923 | +pub async fn get_tags(path: String) -> std::result::Result<Vec<TagInfo>, String> { |
| 924 | + let repo = Repository::open(&path).map_err(|e| e.to_string())?; |
| 925 | + get_tags_impl(&repo).map_err(|e| e.to_string()) |
| 926 | +} |
| 927 | + |
| 928 | +fn get_tags_impl(repo: &Repository) -> Result<Vec<TagInfo>> { |
| 929 | + let tag_names = repo.tag_names(None)?; |
| 930 | + let mut tags = Vec::new(); |
| 931 | + |
| 932 | + for name in tag_names.iter().flatten() { |
| 933 | + if let Ok(obj) = repo.revparse_single(name) { |
| 934 | + let mut message: Option<String> = None; |
| 935 | + let mut tagger: Option<String> = None; |
| 936 | + let mut date: Option<i64> = None; |
| 937 | + |
| 938 | + // Check if it's an annotated tag |
| 939 | + if let Some(tag) = obj.as_tag() { |
| 940 | + message = tag.message().map(|s| s.to_string()); |
| 941 | + if let Some(sig) = tag.tagger() { |
| 942 | + tagger = sig.name().map(|s| s.to_string()); |
| 943 | + date = Some(sig.when().seconds()); |
| 944 | + } |
| 945 | + } else if let Ok(_commit) = obj.peel_to_commit() { |
| 946 | + // Lightweight tag points directly to commit |
| 947 | + } |
| 948 | + |
| 949 | + // Target commit SHA |
| 950 | + let target = obj.peel_to_commit()?.id().to_string(); |
| 951 | + |
| 952 | + tags.push(TagInfo { |
| 953 | + name: name.to_string(), |
| 954 | + message, |
| 955 | + target, |
| 956 | + tagger, |
| 957 | + date, |
| 958 | + }); |
| 959 | + } |
| 960 | + } |
| 961 | + |
| 962 | + // Sort tags by date (descending) or name |
| 963 | + tags.sort_by(|a, b| { |
| 964 | + // Prefer date if available |
| 965 | + match (a.date, b.date) { |
| 966 | + (Some(da), Some(db)) => db.cmp(&da), // Descending |
| 967 | + _ => b.name.cmp(&a.name), // Fallback to name |
| 968 | + } |
| 969 | + }); |
| 970 | + |
| 971 | + Ok(tags) |
| 972 | +} |
| 973 | + |
| 974 | +/// Create a new tag |
| 975 | +#[tauri::command] |
| 976 | +pub async fn create_tag( |
| 977 | + path: String, |
| 978 | + name: String, |
| 979 | + message: Option<String>, |
| 980 | + target: Option<String>, |
| 981 | +) -> std::result::Result<(), String> { |
| 982 | + let repo = Repository::open(&path).map_err(|e| e.to_string())?; |
| 983 | + create_tag_impl(&repo, &name, message, target).map_err(|e| e.to_string()) |
| 984 | +} |
| 985 | + |
| 986 | +fn create_tag_impl(repo: &Repository, name: &str, message: Option<String>, target: Option<String>) -> Result<()> { |
| 987 | + // Get the target object |
| 988 | + let obj = if let Some(oid_str) = target { |
| 989 | + repo.find_object(git2::Oid::from_str(&oid_str)?, None)? |
| 990 | + } else { |
| 991 | + repo.head()?.peel(git2::ObjectType::Any)? |
| 992 | + }; |
| 993 | + |
| 994 | + if let Some(msg) = message { |
| 995 | + // Annotated tag |
| 996 | + let signature = repo.signature()?; |
| 997 | + repo.tag(name, &obj, &signature, &msg, false)?; |
| 998 | + } else { |
| 999 | + // Lightweight tag |
| 1000 | + repo.tag_lightweight(name, &obj, false)?; |
| 1001 | + } |
| 1002 | + |
| 1003 | + Ok(()) |
| 1004 | +} |
| 1005 | + |
| 1006 | +/// Delete a tag |
| 1007 | +#[tauri::command] |
| 1008 | +pub async fn delete_tag(path: String, name: String) -> std::result::Result<(), String> { |
| 1009 | + let repo = Repository::open(&path).map_err(|e| e.to_string())?; |
| 1010 | + repo.tag_delete(&name).map_err(|e| e.to_string())?; |
| 1011 | + Ok(()) |
| 1012 | +} |
| 1013 | + |
| 1014 | +/// Push a tag to remote |
| 1015 | +#[tauri::command] |
| 1016 | +pub async fn push_tag( |
| 1017 | + path: String, |
| 1018 | + tag_name: String, |
| 1019 | + remote: String, |
| 1020 | + username: Option<String>, |
| 1021 | + password: Option<String>, |
| 1022 | +) -> std::result::Result<(), String> { |
| 1023 | + let repo = Repository::open(&path).map_err(|e| e.to_string())?; |
| 1024 | + push_tag_impl(&repo, &tag_name, &remote, username, password).map_err(|e| e.to_string()) |
| 1025 | +} |
| 1026 | + |
| 1027 | +fn push_tag_impl( |
| 1028 | + repo: &Repository, |
| 1029 | + tag_name: &str, |
| 1030 | + remote: &str, |
| 1031 | + username: Option<String>, |
| 1032 | + password: Option<String>, |
| 1033 | +) -> Result<()> { |
| 1034 | + let mut remote_obj = repo.find_remote(remote) |
| 1035 | + .map_err(|_| AppError::InvalidInput(format!("Remote '{}' not found", remote)))?; |
| 1036 | + |
| 1037 | + // Refspec for pushing a tag |
| 1038 | + let refspec = format!("refs/tags/{}:refs/tags/{}", tag_name, tag_name); |
| 1039 | + |
| 1040 | + let config = repo.config()?; |
| 1041 | + let auth_username = username.clone(); |
| 1042 | + let auth_password = password.clone(); |
| 1043 | + |
| 1044 | + let mut callbacks = git2::RemoteCallbacks::new(); |
| 1045 | + callbacks.credentials(move |url, username_from_url, allowed_types| { |
| 1046 | + let default_username = username_from_url.unwrap_or("git"); |
| 1047 | + if let (Some(user), Some(pass)) = (&auth_username, &auth_password) { |
| 1048 | + return git2::Cred::userpass_plaintext(user, pass); |
| 1049 | + } |
| 1050 | + if allowed_types.contains(git2::CredentialType::SSH_KEY) { |
| 1051 | + git2::Cred::ssh_key_from_agent(default_username) |
| 1052 | + } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { |
| 1053 | + git2::Cred::credential_helper(&config, url, Some(default_username)) |
| 1054 | + } else if allowed_types.contains(git2::CredentialType::DEFAULT) { |
| 1055 | + git2::Cred::credential_helper(&config, url, Some(default_username)) |
| 1056 | + } else { |
| 1057 | + Err(git2::Error::from_str("no authentication method available")) |
| 1058 | + } |
| 1059 | + }); |
| 1060 | + |
| 1061 | + let mut push_options = git2::PushOptions::new(); |
| 1062 | + push_options.remote_callbacks(callbacks); |
| 1063 | + |
| 1064 | + remote_obj.push(&[&refspec], Some(&mut push_options))?; |
| 1065 | + |
| 1066 | + Ok(()) |
| 1067 | +} |
| 1068 | + |
| 1069 | +/// Delete a remote tag |
| 1070 | +#[tauri::command] |
| 1071 | +pub async fn delete_remote_tag( |
| 1072 | + path: String, |
| 1073 | + tag_name: String, |
| 1074 | + remote: String, |
| 1075 | + username: Option<String>, |
| 1076 | + password: Option<String>, |
| 1077 | +) -> std::result::Result<(), String> { |
| 1078 | + let repo = Repository::open(&path).map_err(|e| e.to_string())?; |
| 1079 | + delete_remote_tag_impl(&repo, &tag_name, &remote, username, password).map_err(|e| e.to_string()) |
| 1080 | +} |
| 1081 | + |
| 1082 | +fn delete_remote_tag_impl( |
| 1083 | + repo: &Repository, |
| 1084 | + tag_name: &str, |
| 1085 | + remote: &str, |
| 1086 | + username: Option<String>, |
| 1087 | + password: Option<String>, |
| 1088 | +) -> Result<()> { |
| 1089 | + let mut remote_obj = repo.find_remote(remote) |
| 1090 | + .map_err(|_| AppError::InvalidInput(format!("Remote '{}' not found", remote)))?; |
| 1091 | + |
| 1092 | + // Refspec for deleting a remote ref |
| 1093 | + let refspec = format!(":refs/tags/{}", tag_name); |
| 1094 | + |
| 1095 | + let config = repo.config()?; |
| 1096 | + let auth_username = username.clone(); |
| 1097 | + let auth_password = password.clone(); |
| 1098 | + |
| 1099 | + let mut callbacks = git2::RemoteCallbacks::new(); |
| 1100 | + callbacks.credentials(move |url, username_from_url, allowed_types| { |
| 1101 | + let default_username = username_from_url.unwrap_or("git"); |
| 1102 | + if let (Some(user), Some(pass)) = (&auth_username, &auth_password) { |
| 1103 | + return git2::Cred::userpass_plaintext(user, pass); |
| 1104 | + } |
| 1105 | + if allowed_types.contains(git2::CredentialType::SSH_KEY) { |
| 1106 | + git2::Cred::ssh_key_from_agent(default_username) |
| 1107 | + } else if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) { |
| 1108 | + git2::Cred::credential_helper(&config, url, Some(default_username)) |
| 1109 | + } else if allowed_types.contains(git2::CredentialType::DEFAULT) { |
| 1110 | + git2::Cred::credential_helper(&config, url, Some(default_username)) |
| 1111 | + } else { |
| 1112 | + Err(git2::Error::from_str("no authentication method available")) |
| 1113 | + } |
| 1114 | + }); |
| 1115 | + |
| 1116 | + let mut push_options = git2::PushOptions::new(); |
| 1117 | + push_options.remote_callbacks(callbacks); |
| 1118 | + |
| 1119 | + remote_obj.push(&[&refspec], Some(&mut push_options))?; |
| 1120 | + |
| 1121 | + Ok(()) |
| 1122 | +} |
0 commit comments