use git2::{self, Commit, Oid, Reference, References};
use std::fmt;
use std::hash;
use std::result::Result as RResult;
use error::*;
use error::ErrorKind as EK;
use iter::Messages;
#[derive(PartialEq)]
pub enum IssueRefType {
Any,
Head,
Leaf,
}
impl IssueRefType {
pub fn glob_part(&self) -> &'static str {
match *self {
IssueRefType::Any => "**",
IssueRefType::Head => "head",
IssueRefType::Leaf => "leaves/*",
}
}
pub fn of_ref(refname: &str) -> Option<(Oid, IssueRefType)> {
let mut parts = refname.rsplit('/');
let preliminary_ref_type = match parts.next() {
Some("head") => IssueRefType::Head,
Some(part) => if Self::id_from_str(part).is_some() {
match parts.next() {
Some("leaves") => IssueRefType::Leaf,
_ => return None,
}
} else {
return None
},
None => return None,
};
if let Some(id) = parts.next().and_then(Self::id_from_str) {
if parts.any(|part| part == "dit") {
return Some((id, preliminary_ref_type));
}
}
None
}
fn id_from_str(id: &str) -> Option<Oid> {
if id.len() == 40 {
Oid::from_str(id).ok()
} else {
None
}
}
}
impl fmt::Debug for IssueRefType {
fn fmt(&self, f: &mut fmt::Formatter) -> RResult<(), fmt::Error> {
f.write_str(match self {
&IssueRefType::Any => "Any ref",
&IssueRefType::Head => "Head ref",
&IssueRefType::Leaf => "Leaf ref",
})
}
}
pub struct Issue<'r> {
repo: &'r git2::Repository,
obj: git2::Object<'r>,
}
impl<'r> Issue<'r> {
pub fn new(repo: &'r git2::Repository, id: Oid) -> Result<Self> {
repo.find_object(id, Some(git2::ObjectType::Commit))
.chain_err(|| EK::CannotGetCommitForRev(id.to_string()))
.map(|obj| Issue { repo: repo, obj: obj })
}
pub fn id(&self) -> Oid {
self.obj.id()
}
pub fn initial_message(&self) -> Result<git2::Commit<'r>> {
self.obj
.clone()
.into_commit()
.map_err(|obj| Error::from_kind(EK::CannotGetCommitForRev(obj.id().to_string())))
}
pub fn heads(&self) -> Result<References<'r>> {
let glob = format!("**/dit/{}/head", self.ref_part());
self.repo
.references_glob(&glob)
.chain_err(|| EK::CannotFindIssueHead(self.id()))
}
pub fn local_head(&self) -> Result<Reference<'r>> {
let refname = format!("refs/dit/{}/head", self.ref_part());
self.repo
.find_reference(&refname)
.chain_err(|| EK::CannotFindIssueHead(self.id()))
}
pub fn local_refs(&self, ref_type: IssueRefType) -> Result<References<'r>> {
let glob = format!("refs/dit/{}/{}", self.ref_part(), ref_type.glob_part());
self.repo
.references_glob(&glob)
.chain_err(|| EK::CannotGetReferences(glob))
}
pub fn remote_refs(&self, ref_type: IssueRefType) -> Result<References<'r>> {
let glob = format!("refs/remotes/*/dit/{}/{}", self.ref_part(), ref_type.glob_part());
self.repo
.references_glob(&glob)
.chain_err(|| EK::CannotGetReferences(glob))
}
pub fn all_refs(&self, ref_type: IssueRefType) -> Result<References<'r>> {
let glob = format!("**/dit/{}/{}", self.ref_part(), ref_type.glob_part());
self.repo
.references_glob(&glob)
.chain_err(|| EK::CannotGetReferences(glob))
}
pub fn messages(&self) -> Result<Messages<'r>> {
self.terminated_messages()
.and_then(|mut messages| {
let glob = format!("refs/dit/{}/**", self.ref_part());
messages
.revwalk
.push_glob(glob.as_ref())
.chain_err(|| EK::CannotGetReferences(glob))?;
let glob = format!("refs/remotes/*/dit/{}/**", self.ref_part());
messages
.revwalk
.push_glob(glob.as_ref())
.chain_err(|| EK::CannotGetReferences(glob))?;
Ok(messages)
})
}
pub fn messages_from(&self, message: Oid) -> Result<Messages<'r>> {
self.terminated_messages()
.and_then(|mut messages| {
messages
.revwalk
.push(message)
.chain_err(|| EK::CannotConstructRevwalk)?;
Ok(messages)
})
}
pub fn terminated_messages(&self) -> Result<Messages<'r>> {
Messages::empty(self.repo)
.and_then(|mut messages| {
messages.terminate_at_initial(self)?;
messages.revwalk.simplify_first_parent();
messages.revwalk.set_sorting(git2::Sort::TOPOLOGICAL);
Ok(messages)
})
}
pub fn add_message<'a, A, I, J>(&self,
author: &git2::Signature,
committer: &git2::Signature,
message: A,
tree: &git2::Tree,
parents: I
) -> Result<Commit<'r>>
where A: AsRef<str>,
I: IntoIterator<Item = &'a Commit<'a>, IntoIter = J>,
J: Iterator<Item = &'a Commit<'a>>
{
let parent_vec : Vec<&Commit> = parents.into_iter().collect();
self.repo
.commit(None, author, committer, message.as_ref(), tree, &parent_vec)
.and_then(|id| self.repo.find_commit(id))
.chain_err(|| EK::CannotCreateMessage)
.and_then(|message| self.add_leaf(message.id()).map(|_| message))
}
pub fn update_head(&self, message: Oid, replace: bool) -> Result<Reference<'r>> {
let refname = format!("refs/dit/{}/head", self.ref_part());
let reflogmsg = format!("git-dit: set head reference of {} to {}", self, message);
self.repo
.reference(&refname, message, replace, &reflogmsg)
.chain_err(|| EK::CannotSetReference(refname))
}
pub fn add_leaf(&self, message: Oid) -> Result<Reference<'r>> {
let refname = format!("refs/dit/{}/leaves/{}", self.ref_part(), message);
let reflogmsg = format!("git-dit: new leaf for {}: {}", self, message);
self.repo
.reference(&refname, message, false, &reflogmsg)
.chain_err(|| EK::CannotSetReference(refname))
}
pub fn ref_part(&self) -> String {
self.id().to_string()
}
}
impl<'r> fmt::Display for Issue<'r> {
fn fmt(&self, f: &mut fmt::Formatter) -> RResult<(), fmt::Error> {
write!(f, "{}", self.id())
}
}
impl<'r> PartialEq for Issue<'r> {
fn eq(&self, other: &Self) -> bool {
self.id() == other.id()
}
}
impl<'r> Eq for Issue<'r> {}
impl<'r> hash::Hash for Issue<'r> {
fn hash<H>(&self, state: &mut H)
where H: hash::Hasher
{
self.id().hash(state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_utils::TestingRepo;
use repository::RepositoryExt;
#[test]
fn ref_identification() {
{
let (id, reftype) = IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/head")
.expect("Expected valid issue id and ref type");
assert_eq!(id.to_string(), "65b56706fdc3501749d008750c61a1f24b888f72");
assert_eq!(reftype, IssueRefType::Head);
}
{
let (id, reftype) = IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/leaves/f6bd121bdc2ba5906e412da19191a2eaf2025755")
.expect("Expected valid issue id and ref type");
assert_eq!(id.to_string(), "65b56706fdc3501749d008750c61a1f24b888f72");
assert_eq!(reftype, IssueRefType::Leaf);
}
assert!(IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/foo/f6bd121bdc2ba5906e412da19191a2eaf2025755").is_none());
assert!(IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/head/foo").is_none());
assert!(IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/leaves/foo").is_none());
assert!(IssueRefType::of_ref("refs/dit/foo/leaves/f6bd121bdc2ba5906e412da19191a2eaf2025755").is_none());
assert!(IssueRefType::of_ref("refs/dit/foo/head").is_none());
assert!(IssueRefType::of_ref("refs/foo/65b56706fdc3501749d008750c61a1f24b888f72/head").is_none());
assert!(IssueRefType::of_ref("refs/foo/65b56706fdc3501749d008750c61a1f24b888f72/leaves/f6bd121bdc2ba5906e412da19191a2eaf2025755").is_none());
}
#[test]
fn issue_leaves() {
let mut testing_repo = TestingRepo::new("issue_leaves");
let repo = testing_repo.repo();
let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
.expect("Could not create signature");
let empty_tree = repo
.empty_tree()
.expect("Could not create empty tree");
{
let issue = repo
.create_issue(&sig, &sig, "Test message 1", &empty_tree, vec![])
.expect("Could not create issue");
let initial_message = issue
.initial_message()
.expect("Could not retrieve initial message");
issue.add_message(&sig, &sig, "Test message 2", &empty_tree, vec![&initial_message])
.expect("Could not add message");
}
let issue = repo
.create_issue(&sig, &sig, "Test message 3", &empty_tree, vec![])
.expect("Could not create issue");
let initial_message = issue
.initial_message()
.expect("Could not retrieve initial message");
let message = issue
.add_message(&sig, &sig, "Test message 4", &empty_tree, vec![&initial_message])
.expect("Could not add message");
let mut leaves = issue
.local_refs(IssueRefType::Leaf)
.expect("Could not retrieve issue leaves");
let leaf = leaves
.next()
.expect("Could not find leaf reference")
.expect("Could not retrieve leaf reference")
.target()
.expect("Could not determine the target of the leaf reference");
assert_eq!(leaf, message.id());
assert!(leaves.next().is_none());
}
#[test]
fn local_refs() {
let mut testing_repo = TestingRepo::new("local_refs");
let repo = testing_repo.repo();
let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
.expect("Could not create signature");
let empty_tree = repo
.empty_tree()
.expect("Could not create empty tree");
{
let issue = repo
.create_issue(&sig, &sig, "Test message 1", &empty_tree, vec![])
.expect("Could not create issue");
let initial_message = issue
.initial_message()
.expect("Could not retrieve initial message");
issue.add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message])
.expect("Could not add message");
}
let issue = repo
.create_issue(&sig, &sig, "Test message 2", &empty_tree, vec![])
.expect("Could not create issue");
let initial_message = issue
.initial_message()
.expect("Could not retrieve initial message");
let message = issue
.add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message])
.expect("Could not add message");
let mut ids = vec![issue.id(), message.id()];
ids.sort();
let mut ref_ids: Vec<Oid> = issue
.local_refs(IssueRefType::Any)
.expect("Could not retrieve local refs")
.map(|reference| reference.unwrap().target().unwrap())
.collect();
ref_ids.sort();
assert_eq!(ref_ids, ids);
}
#[test]
fn message_revwalk() {
let mut testing_repo = TestingRepo::new("message_revwalk");
let repo = testing_repo.repo();
let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
.expect("Could not create signature");
let empty_tree = repo
.empty_tree()
.expect("Could not create empty tree");
let issue1 = repo
.create_issue(&sig, &sig, "Test message 1", &empty_tree, vec![])
.expect("Could not create issue");
let initial_message1 = issue1
.initial_message()
.expect("Could not retrieve initial message");
let issue2 = repo
.create_issue(&sig, &sig, "Test message 2", &empty_tree, vec![&initial_message1])
.expect("Could not create issue");
let initial_message2 = issue2
.initial_message()
.expect("Could not retrieve initial message");
let message = issue2
.add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message2])
.expect("Could not add message");
let message_id = message.id();
let mut iter1 = issue1
.messages()
.expect("Could not create message revwalk iterator");
assert_eq!(iter1.next().unwrap().unwrap().id(), issue1.id());
assert!(iter1.next().is_none());
let mut iter2 = issue2
.messages()
.expect("Could not create message revwalk iterator");
assert_eq!(iter2.next().unwrap().unwrap().id(), message_id);
assert_eq!(iter2.next().unwrap().unwrap().id(), issue2.id());
assert!(iter2.next().is_none());
}
#[test]
fn update_head() {
let mut testing_repo = TestingRepo::new("update_head");
let repo = testing_repo.repo();
let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
.expect("Could not create signature");
let empty_tree = repo
.empty_tree()
.expect("Could not create empty tree");
let issue = repo
.create_issue(&sig, &sig, "Test message 2", &empty_tree, vec![])
.expect("Could not create issue");
let initial_message = issue
.initial_message()
.expect("Could not retrieve initial message");
let message = issue
.add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message])
.expect("Could not add message");
assert_eq!(issue.local_head().unwrap().target().unwrap(), issue.id());
issue
.update_head(message.id(), true)
.expect("Could not update head reference");
assert_eq!(issue.local_head().unwrap().target().unwrap(), message.id());
}
}