gitui/asyncgit/src/sync/commit.rs
2021-06-03 23:55:30 +02:00

338 lines
9.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::{utils::repo, CommitId};
use crate::{error::Result, sync::utils::get_head_repo};
use git2::{ErrorCode, ObjectType, Repository, Signature};
use scopetime::scope_time;
///
pub fn amend(
repo_path: &str,
id: CommitId,
msg: &str,
) -> Result<CommitId> {
scope_time!("amend");
let repo = repo(repo_path)?;
let commit = repo.find_commit(id.into())?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let new_id = commit.amend(
Some("HEAD"),
None,
None,
None,
Some(msg),
Some(&tree),
)?;
Ok(CommitId::new(new_id))
}
/// Wrap `Repository::signature` to allow unknown user.name.
///
/// See <https://github.com/extrawurst/gitui/issues/79>.
#[allow(clippy::redundant_pub_crate)]
pub(crate) fn signature_allow_undefined_name(
repo: &Repository,
) -> std::result::Result<Signature<'_>, git2::Error> {
let signature = repo.signature();
if let Err(ref e) = signature {
if e.code() == ErrorCode::NotFound {
let config = repo.config()?;
if let (Err(_), Ok(email_entry)) = (
config.get_entry("user.name"),
config.get_entry("user.email"),
) {
if let Some(email) = email_entry.value() {
return Signature::now("unknown", email);
}
};
}
}
signature
}
/// this does not run any git hooks
pub fn commit(repo_path: &str, msg: &str) -> Result<CommitId> {
scope_time!("commit");
let repo = repo(repo_path)?;
let signature = signature_allow_undefined_name(&repo)?;
let mut index = repo.index()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let parents = if let Ok(id) = get_head_repo(&repo) {
vec![repo.find_commit(id.into())?]
} else {
Vec::new()
};
let parents = parents.iter().collect::<Vec<_>>();
Ok(repo
.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
parents.as_slice(),
)?
.into())
}
/// Tag a commit.
///
/// This function will return an `Err(…)` variant if the tags name is refused
/// by git or if the tag already exists.
pub fn tag(
repo_path: &str,
commit_id: &CommitId,
tag: &str,
) -> Result<CommitId> {
scope_time!("tag");
let repo = repo(repo_path)?;
let signature = signature_allow_undefined_name(&repo)?;
let object_id = commit_id.get_oid();
let target =
repo.find_object(object_id, Some(ObjectType::Commit))?;
Ok(repo.tag(tag, &target, &signature, "", false)?.into())
}
#[cfg(test)]
mod tests {
use crate::error::Result;
use crate::sync::{
commit, get_commit_details, get_commit_files, stage_add_file,
tags::get_tags,
tests::{get_statuses, repo_init, repo_init_empty},
utils::get_head,
LogWalker,
};
use commit::{amend, tag};
use git2::Repository;
use std::{fs::File, io::Write, path::Path};
fn count_commits(repo: &Repository, max: usize) -> usize {
let mut items = Vec::new();
let mut walk = LogWalker::new(&repo, max);
walk.read(&mut items).unwrap();
items.len()
}
#[test]
fn test_commit() {
let file_path = Path::new("foo");
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))
.unwrap()
.write_all(b"test\nfoo")
.unwrap();
assert_eq!(get_statuses(repo_path), (1, 0));
stage_add_file(repo_path, file_path).unwrap();
assert_eq!(get_statuses(repo_path), (0, 1));
commit(repo_path, "commit msg").unwrap();
assert_eq!(get_statuses(repo_path), (0, 0));
}
#[test]
fn test_commit_in_empty_repo() {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
assert_eq!(get_statuses(repo_path), (0, 0));
File::create(&root.join(file_path))
.unwrap()
.write_all(b"test\nfoo")
.unwrap();
assert_eq!(get_statuses(repo_path), (1, 0));
stage_add_file(repo_path, file_path).unwrap();
assert_eq!(get_statuses(repo_path), (0, 1));
commit(repo_path, "commit msg").unwrap();
assert_eq!(get_statuses(repo_path), (0, 0));
}
#[test]
fn test_amend() -> Result<()> {
let file_path1 = Path::new("foo");
let file_path2 = Path::new("foo2");
let (_td, repo) = repo_init_empty()?;
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path1))?.write_all(b"test1")?;
stage_add_file(repo_path, file_path1)?;
let id = commit(repo_path, "commit msg")?;
assert_eq!(count_commits(&repo, 10), 1);
File::create(&root.join(file_path2))?.write_all(b"test2")?;
stage_add_file(repo_path, file_path2)?;
let new_id = amend(repo_path, id, "amended")?;
assert_eq!(count_commits(&repo, 10), 1);
let details = get_commit_details(repo_path, new_id)?;
assert_eq!(details.message.unwrap().subject, "amended");
let files = get_commit_files(repo_path, new_id)?;
assert_eq!(files.len(), 2);
let head = get_head(repo_path)?;
assert_eq!(head, new_id);
Ok(())
}
#[test]
fn test_tag() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))?
.write_all(b"test\nfoo")?;
stage_add_file(repo_path, file_path)?;
let new_id = commit(repo_path, "commit msg")?;
tag(repo_path, &new_id, "tag")?;
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec!["tag"]
);
assert!(matches!(tag(repo_path, &new_id, "tag"), Err(_)));
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec!["tag"]
);
tag(repo_path, &new_id, "second-tag")?;
assert_eq!(
get_tags(repo_path).unwrap()[&new_id],
vec!["second-tag", "tag"]
);
Ok(())
}
/// Beware: this test has to be run with a `$HOME/.gitconfig` that has
/// `user.email` not set. Otherwise, git falls back to the value of
/// `user.email` in `$HOME/.gitconfig` and this test fails.
///
/// As of February 2021, `repo_init_empty` sets all git config locations
/// to an empty temporary directory, so this constraint is met.
#[test]
fn test_empty_email() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))?
.write_all(b"test\nfoo")?;
stage_add_file(repo_path, file_path)?;
repo.config()?.remove("user.email")?;
let error = commit(repo_path, "commit msg");
assert!(matches!(error, Err(_)));
repo.config()?.set_str("user.email", "email")?;
let success = commit(repo_path, "commit msg");
assert!(matches!(success, Ok(_)));
assert_eq!(count_commits(&repo, 10), 1);
let details =
get_commit_details(repo_path, success.unwrap()).unwrap();
assert_eq!(details.author.name, "name");
assert_eq!(details.author.email, "email");
Ok(())
}
/// See comment to `test_empty_email`.
#[test]
fn test_empty_name() -> Result<()> {
let file_path = Path::new("foo");
let (_td, repo) = repo_init_empty().unwrap();
let root = repo.path().parent().unwrap();
let repo_path = root.as_os_str().to_str().unwrap();
File::create(&root.join(file_path))?
.write_all(b"test\nfoo")?;
stage_add_file(repo_path, file_path)?;
repo.config()?.remove("user.name")?;
let mut success = commit(repo_path, "commit msg");
assert!(matches!(success, Ok(_)));
assert_eq!(count_commits(&repo, 10), 1);
let mut details =
get_commit_details(repo_path, success.unwrap()).unwrap();
assert_eq!(details.author.name, "unknown");
assert_eq!(details.author.email, "email");
repo.config()?.set_str("user.name", "name")?;
success = commit(repo_path, "commit msg");
assert!(matches!(success, Ok(_)));
assert_eq!(count_commits(&repo, 10), 2);
details =
get_commit_details(repo_path, success.unwrap()).unwrap();
assert_eq!(details.author.name, "name");
assert_eq!(details.author.email, "email");
Ok(())
}
}