use git2::{Repository, StatusOptions, StatusShow};
use super::git_helpers;
use super::COMMIT_ID_SHORT_HASH_LENGTH;
#[derive(Clone, PartialEq, Eq)]
pub struct TagInfoOwned {
pub tag: String,
pub commits_since_tag: u32,
}
#[derive(Clone, PartialEq, Eq)]
pub struct GitInfoOwned {
pub tag_info: Option<TagInfoOwned>,
pub commit_id: String,
pub modified: bool,
}
pub fn get_git_info(repo: &Repository) -> Result<GitInfoOwned, git2::Error> {
let head_commit = repo.head()?.peel_to_commit()?;
let head_commit_id_str = head_commit.id().to_string();
let head_commit_id_str = head_commit_id_str[..COMMIT_ID_SHORT_HASH_LENGTH].to_string();
let modified = {
let statuses = repo.statuses(Some(
StatusOptions::default()
.show(StatusShow::IndexAndWorkdir)
.include_untracked(false)
.include_ignored(false)
.include_unmodified(false)
.exclude_submodules(false),
))?;
statuses.iter().any(|status| {
status.status() != git2::Status::CURRENT && status.status() != git2::Status::IGNORED
})
};
let all_tags = git_helpers::all_tags(repo)?;
let mut current_commit = head_commit;
let mut commits_since_tag = 0;
loop {
let commit_id = current_commit.id();
if let Some(tags) = all_tags.get(&commit_id) {
let tag = tags.first().expect(
"tag list can't be empty, because the `all_tags` HashMap only contains entries that have at least one element",
);
return Ok(GitInfoOwned {
tag_info: Some(TagInfoOwned {
tag: tag.to_string(),
commits_since_tag,
}),
commit_id: head_commit_id_str,
modified,
});
}
match current_commit.parent(0) {
Ok(parent) => current_commit = parent,
Err(_) => {
return Ok(GitInfoOwned {
tag_info: None,
commit_id: head_commit_id_str,
modified,
});
}
}
commits_since_tag += 1;
}
}
#[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) -> git2::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) -> git2::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) {
let head_commit = repo.head().unwrap().peel(git2::ObjectType::Commit).unwrap();
repo.tag_lightweight(tag_name, &head_commit, true).unwrap();
}
fn add_to_index(repo: &Repository) {
let mut index = repo.index().unwrap();
index
.add_all(["*"], git2::IndexAddOption::DEFAULT, None)
.unwrap();
index.write().unwrap();
}
#[test]
fn commit_id_has_correct_length() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
let info = get_git_info(&repo).unwrap();
assert_eq!(info.commit_id.len(), COMMIT_ID_SHORT_HASH_LENGTH);
}
#[test]
fn no_tags_clean_workdir() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
let info = get_git_info(&repo).unwrap();
assert!(info.tag_info.is_none());
assert!(!info.modified);
}
#[test]
fn no_tags_dirty_workdir() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
std::fs::write(repo.workdir().unwrap().join("file.txt"), "modified").unwrap();
let info = get_git_info(&repo).unwrap();
assert!(info.tag_info.is_none());
assert!(info.modified);
}
#[test]
fn on_tag_not_modified() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
create_tag(&repo, "v1.0.0");
let info = get_git_info(&repo).unwrap();
assert!(info.tag_info.is_some());
let tag_info = info.tag_info.unwrap();
assert_eq!(tag_info.tag, "v1.0.0");
assert_eq!(tag_info.commits_since_tag, 0);
assert!(!info.modified);
}
#[test]
fn commits_after_tag() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
create_tag(&repo, "v1.0.0");
create_commit(&repo, "second");
create_commit(&repo, "third");
create_commit(&repo, "fourth");
let info = get_git_info(&repo).unwrap();
assert!(info.tag_info.is_some());
let tag_info = info.tag_info.unwrap();
assert_eq!(tag_info.tag, "v1.0.0");
assert_eq!(tag_info.commits_since_tag, 3);
}
#[test]
fn untracked_files_not_counted_as_modified() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
std::fs::write(repo.workdir().unwrap().join("untracked.txt"), "new file").unwrap();
let info = get_git_info(&repo).unwrap();
assert!(!info.modified);
}
#[test]
fn staged_changes_count_as_modified() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
create_initial_commit(&repo);
std::fs::write(repo.workdir().unwrap().join("file.txt"), "staged changes").unwrap();
add_to_index(&repo);
let info = get_git_info(&repo).unwrap();
assert!(info.modified);
}
#[test]
fn empty_repo_returns_error() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let result = get_git_info(&repo);
assert!(result.is_err());
}
#[test]
fn commit_id_is_prefix_of_full_hash() {
let dir = TempDir::new("test").unwrap();
let repo = create_repo(dir.path());
let full_oid = create_initial_commit(&repo);
let full_hash = full_oid.to_string();
let info = get_git_info(&repo).unwrap();
assert!(full_hash.starts_with(&info.commit_id));
}
}