use git2::{Oid, Repository};
use std::collections::hash_map::{Entry, HashMap};
pub fn all_tags(repo: &Repository) -> Result<HashMap<Oid, Vec<String>>, git2::Error> {
let mut result: HashMap<Oid, Vec<String>> = HashMap::new();
let mut error = None;
repo.tag_foreach(|commit_id, name| {
let name = std::str::from_utf8(name)
.map_err(|err| git2::Error::from_str(&format!("Tag name is not valid UTF-8: {}", err)));
let name = match name {
Ok(name) => name,
Err(err) => {
assert!(
error.is_none(),
"We immediately exit after an error so this can't be set yet"
);
error = Some(err);
return false;
}
};
let name = name.strip_prefix("refs/tags/").ok_or_else(|| {
git2::Error::from_str(&format!(
"Tag name '{}' doesn't start with 'refs/tags/'",
name
))
});
let name = match name {
Ok(name) => name,
Err(err) => {
assert!(
error.is_none(),
"We immediately exit after an error so this can't be set yet"
);
error = Some(err);
return false;
}
};
let name = name.to_owned();
match result.entry(commit_id) {
Entry::Occupied(mut entry) => {
entry.get_mut().push(name);
}
Entry::Vacant(entry) => {
entry.insert(vec![name]);
}
}
true
})?;
if let Some(error) = error {
Err(error)
} else {
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
fn create_repo(path: &std::path::Path) -> Repository {
let repo = Repository::init(path).unwrap();
repo.config()
.unwrap()
.set_str("user.name", "Test User")
.unwrap();
repo.config()
.unwrap()
.set_str("user.email", "test@example.com")
.unwrap();
repo
}
fn create_initial_commit(repo: &Repository) -> Oid {
let content = "initial content";
std::fs::write(repo.workdir().unwrap().join("file.txt"), content).unwrap();
let mut index = repo.index().unwrap();
index
.add_all(["*"], git2::IndexAddOption::DEFAULT, None)
.unwrap();
index.write().unwrap();
let sig = repo.signature().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[])
.unwrap()
}
fn create_commit(repo: &Repository, content: &str) -> Oid {
std::fs::write(repo.workdir().unwrap().join("file.txt"), content).unwrap();
let mut index = repo.index().unwrap();
index
.add_all(["*"], git2::IndexAddOption::DEFAULT, None)
.unwrap();
index.write().unwrap();
let sig = repo.signature().unwrap();
let tree_id = index.write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let head_commit = repo.head().unwrap().peel_to_commit().unwrap();
repo.commit(
Some("HEAD"),
&sig,
&sig,
&format!("Commit: {}", content),
&tree,
&[&head_commit],
)
.unwrap()
}
fn create_tag(repo: &Repository, tag_name: &str) -> Oid {
let head_commit = repo.head().unwrap().peel(git2::ObjectType::Commit).unwrap();
repo.tag_lightweight(tag_name, &head_commit, true).unwrap()
}
#[test]
fn repo_with_no_commits_returns_empty_hashmap() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let tags = all_tags(&repo).unwrap();
assert!(tags.is_empty());
}
#[test]
fn repo_with_commits_but_no_tags() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
create_commit(&repo, "second");
let tags = all_tags(&repo).unwrap();
assert!(tags.is_empty());
}
#[test]
fn repo_with_single_tag() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let commit_oid = create_initial_commit(&repo);
create_tag(&repo, "v1.0.0");
let tags = all_tags(&repo).unwrap();
assert_eq!(tags.len(), 1);
assert!(tags.contains_key(&commit_oid));
assert_eq!(tags[&commit_oid], vec!["v1.0.0"]);
}
#[test]
fn multiple_tags_on_different_commits() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let first_commit = create_initial_commit(&repo);
create_tag(&repo, "v1.0.0");
let second_commit = create_commit(&repo, "second");
create_tag(&repo, "v2.0.0");
let tags = all_tags(&repo).unwrap();
assert_eq!(tags.len(), 2);
assert_eq!(tags[&first_commit], vec!["v1.0.0"]);
assert_eq!(tags[&second_commit], vec!["v2.0.0"]);
}
#[test]
fn multiple_tags_on_same_commit() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let commit_oid = create_initial_commit(&repo);
create_tag(&repo, "v1.0.0");
create_tag(&repo, "release-1.0");
let tags = all_tags(&repo).unwrap();
assert_eq!(tags.len(), 1);
assert!(tags.contains_key(&commit_oid));
let tag_names = &tags[&commit_oid];
assert_eq!(tag_names.len(), 2);
assert!(tag_names.contains(&"v1.0.0".to_string()));
assert!(tag_names.contains(&"release-1.0".to_string()));
}
#[test]
fn tag_names_without_v_prefix() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let commit_oid = create_initial_commit(&repo);
create_tag(&repo, "release-1.0");
let tags = all_tags(&repo).unwrap();
assert_eq!(tags[&commit_oid], vec!["release-1.0"]);
}
#[test]
fn tag_with_special_characters() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let commit_oid = create_initial_commit(&repo);
create_tag(&repo, "v1.0.0-beta.1_test");
let tags = all_tags(&repo).unwrap();
assert_eq!(tags[&commit_oid], vec!["v1.0.0-beta.1_test"]);
}
}