Skip to main content

libgitdit/
issue.rs

1// git-dit - the distributed issue tracker for git
2// Copyright (C) 2016, 2017 Matthias Beyer <mail@beyermatthias.de>
3// Copyright (C) 2016, 2017 Julian Ganz <neither@nut.email>
4//
5// This Source Code Form is subject to the terms of the Mozilla Public
6// License, v. 2.0. If a copy of the MPL was not distributed with this
7// file, You can obtain one at http://mozilla.org/MPL/2.0/.
8//
9
10//! Issues
11//!
12//! This module provides the `Issue` type and related functionality.
13//!
14
15use git2::{self, Commit, Oid, Reference, References};
16use std::fmt;
17use std::hash;
18use std::result::Result as RResult;
19
20use error::*;
21use error::ErrorKind as EK;
22use iter::Messages;
23
24
25#[derive(PartialEq)]
26pub enum IssueRefType {
27    Any,
28    Head,
29    Leaf,
30}
31
32impl IssueRefType {
33    /// Get the part of a glob specific to the type
34    ///
35    pub fn glob_part(&self) -> &'static str {
36        match *self {
37            IssueRefType::Any   => "**",
38            IssueRefType::Head  => "head",
39            IssueRefType::Leaf  => "leaves/*",
40        }
41    }
42
43    /// Get the issue ref type assiciated with a reference
44    ///
45    /// This functio ndetermines the issue ref type and returns th type as well
46    /// as the issue id as a bonus. If the type of reference could not be
47    /// determined or the ref doesn't appear to belong into the dit context,
48    /// this function returns `None`.
49    ///
50    pub fn of_ref(refname: &str) -> Option<(Oid, IssueRefType)> {
51        let mut parts = refname.rsplit('/');
52
53        // The ref type is denominated by the last few elements.
54        let preliminary_ref_type = match parts.next() {
55            Some("head") => IssueRefType::Head,
56            Some(part) => if Self::id_from_str(part).is_some() {
57                // The last element might be an id, in which case the second
58                // last part should tell us the meaning of the id.
59                match parts.next() {
60                    Some("leaves") => IssueRefType::Leaf,
61                    _ => return None,
62                }
63            } else {
64                return None
65            },
66            None => return None,
67        };
68
69        // The denominating end of the reference is preceeded by an issue id of
70        // some sort.
71        if let Some(id) = parts.next().and_then(Self::id_from_str) {
72            // A dit reference also has to contain a "dit" denominator at some
73            // point.
74            if parts.any(|part| part == "dit") {
75                return Some((id, preliminary_ref_type));
76            }
77        }
78
79        None
80    }
81
82    /// Create an Oid from a full 40-character representation
83    ///
84    /// If the number of characters is not exactly 40 or the string is not an
85    /// Oid-representation, `None` is returned.
86    ///
87    fn id_from_str(id: &str) -> Option<Oid> {
88        if id.len() == 40 {
89            Oid::from_str(id).ok()
90        } else {
91            None
92        }
93    }
94}
95
96impl fmt::Debug for IssueRefType {
97    fn fmt(&self, f: &mut fmt::Formatter) -> RResult<(), fmt::Error> {
98        f.write_str(match self {
99            &IssueRefType::Any   => "Any ref",
100            &IssueRefType::Head  => "Head ref",
101            &IssueRefType::Leaf  => "Leaf ref",
102        })
103    }
104}
105
106
107/// Issue handle
108///
109/// Instances of this type represent single issues. Issues reside in
110/// repositories and are uniquely identified by an id.
111///
112pub struct Issue<'r> {
113    repo: &'r git2::Repository,
114    obj: git2::Object<'r>,
115}
116
117impl<'r> Issue<'r> {
118    /// Create a new handle for an issue with a given id
119    ///
120    pub fn new(repo: &'r git2::Repository, id: Oid) -> Result<Self> {
121        repo.find_object(id, Some(git2::ObjectType::Commit))
122            .chain_err(|| EK::CannotGetCommitForRev(id.to_string()))
123            .map(|obj| Issue { repo: repo, obj: obj })
124    }
125
126    /// Get the issue's id
127    ///
128    pub fn id(&self) -> Oid {
129        self.obj.id()
130    }
131
132    /// Get the issue's initial message
133    ///
134    pub fn initial_message(&self) -> Result<git2::Commit<'r>> {
135        self.obj
136            .clone()
137            .into_commit()
138            .map_err(|obj| Error::from_kind(EK::CannotGetCommitForRev(obj.id().to_string())))
139    }
140
141    /// Get possible heads of the issue
142    ///
143    /// Returns the head references from both the local repository and remotes
144    /// for this issue.
145    ///
146    pub fn heads(&self) -> Result<References<'r>> {
147        let glob = format!("**/dit/{}/head", self.ref_part());
148        self.repo
149            .references_glob(&glob)
150            .chain_err(|| EK::CannotFindIssueHead(self.id()))
151    }
152
153    /// Get the local issue head for the issue
154    ///
155    /// Returns the head reference of the issue from the local repository, if
156    /// present.
157    ///
158    pub fn local_head(&self) -> Result<Reference<'r>> {
159        let refname = format!("refs/dit/{}/head", self.ref_part());
160        self.repo
161            .find_reference(&refname)
162            .chain_err(|| EK::CannotFindIssueHead(self.id()))
163    }
164
165    /// Get local references for the issue
166    ///
167    /// Return all references of a specific type associated with the issue from
168    /// the local repository.
169    ///
170    pub fn local_refs(&self, ref_type: IssueRefType) -> Result<References<'r>> {
171        let glob = format!("refs/dit/{}/{}", self.ref_part(), ref_type.glob_part());
172        self.repo
173            .references_glob(&glob)
174            .chain_err(|| EK::CannotGetReferences(glob))
175    }
176
177    /// Get remote references for the issue
178    ///
179    /// Return all references of a specific type associated with the issue from
180    /// all remote repositories.
181    ///
182    pub fn remote_refs(&self, ref_type: IssueRefType) -> Result<References<'r>> {
183        let glob = format!("refs/remotes/*/dit/{}/{}", self.ref_part(), ref_type.glob_part());
184        self.repo
185            .references_glob(&glob)
186            .chain_err(|| EK::CannotGetReferences(glob))
187    }
188
189    /// Get references for the issue
190    ///
191    /// Return all references of a specific type associated with the issue from
192    /// both the local and remote repositories.
193    ///
194    pub fn all_refs(&self, ref_type: IssueRefType) -> Result<References<'r>> {
195        let glob = format!("**/dit/{}/{}", self.ref_part(), ref_type.glob_part());
196        self.repo
197            .references_glob(&glob)
198            .chain_err(|| EK::CannotGetReferences(glob))
199    }
200
201    /// Get all Messages of the issue
202    ///
203    /// The sorting of the underlying revwalk will be set to "topological".
204    ///
205    pub fn messages(&self) -> Result<Messages<'r>> {
206        self.terminated_messages()
207            .and_then(|mut messages| {
208                // The iterator will iterate over all the messages in the tree
209                // spanned but it will halt at the initial message.
210                let glob = format!("refs/dit/{}/**", self.ref_part());
211                messages
212                    .revwalk
213                    .push_glob(glob.as_ref())
214                    .chain_err(|| EK::CannotGetReferences(glob))?;
215
216                let glob = format!("refs/remotes/*/dit/{}/**", self.ref_part());
217                messages
218                    .revwalk
219                    .push_glob(glob.as_ref())
220                    .chain_err(|| EK::CannotGetReferences(glob))?;
221
222                Ok(messages)
223            })
224    }
225
226    /// Get Messages of the issue starting from a specific one
227    ///
228    /// The Messages iterator returned will return all first parents up to and
229    /// includingthe initial message of the issue.
230    ///
231    pub fn messages_from(&self, message: Oid) -> Result<Messages<'r>> {
232        self.terminated_messages()
233            .and_then(|mut messages| {
234                messages
235                    .revwalk
236                    .push(message)
237                    .chain_err(|| EK::CannotConstructRevwalk)?;
238
239                Ok(messages)
240            })
241    }
242
243    /// Prepare a Messages iterator which will terminate at the initial message
244    ///
245    pub fn terminated_messages(&self) -> Result<Messages<'r>> {
246        Messages::empty(self.repo)
247            .and_then(|mut messages| {
248                // terminate at this issue's initial message
249                messages.terminate_at_initial(self)?;
250
251                // configure the revwalk
252                messages.revwalk.simplify_first_parent();
253                messages.revwalk.set_sorting(git2::Sort::TOPOLOGICAL);
254
255                Ok(messages)
256            })
257    }
258
259    /// Add a new message to the issue
260    ///
261    /// Adds a new message to the issue. Also create a leaf reference for the
262    /// new message. Returns the message.
263    ///
264    pub fn add_message<'a, A, I, J>(&self,
265                                    author: &git2::Signature,
266                                    committer: &git2::Signature,
267                                    message: A,
268                                    tree: &git2::Tree,
269                                    parents: I
270    ) -> Result<Commit<'r>>
271        where A: AsRef<str>,
272              I: IntoIterator<Item = &'a Commit<'a>, IntoIter = J>,
273              J: Iterator<Item = &'a Commit<'a>>
274    {
275        let parent_vec : Vec<&Commit> = parents.into_iter().collect();
276
277        self.repo
278            .commit(None, author, committer, message.as_ref(), tree, &parent_vec)
279            .and_then(|id| self.repo.find_commit(id))
280            .chain_err(|| EK::CannotCreateMessage)
281            .and_then(|message| self.add_leaf(message.id()).map(|_| message))
282    }
283
284    /// Update the local head reference of the issue
285    ///
286    /// Updates the local head reference of the issue to the provided message.
287    ///
288    /// # Warnings
289    ///
290    /// The function will update the reference even if it would not be an
291    /// fast-forward update.
292    ///
293    pub fn update_head(&self, message: Oid, replace: bool) -> Result<Reference<'r>> {
294        let refname = format!("refs/dit/{}/head", self.ref_part());
295        let reflogmsg = format!("git-dit: set head reference of {} to {}", self, message);
296        self.repo
297            .reference(&refname, message, replace, &reflogmsg)
298            .chain_err(|| EK::CannotSetReference(refname))
299    }
300
301    /// Add a new leaf reference associated with the issue
302    ///
303    /// Creates a new leaf reference for the message provided in the issue.
304    ///
305    pub fn add_leaf(&self, message: Oid) -> Result<Reference<'r>> {
306        let refname = format!("refs/dit/{}/leaves/{}", self.ref_part(), message);
307        let reflogmsg = format!("git-dit: new leaf for {}: {}", self, message);
308        self.repo
309            .reference(&refname, message, false, &reflogmsg)
310            .chain_err(|| EK::CannotSetReference(refname))
311    }
312
313    /// Get reference part for this issue
314    ///
315    /// The references associated with an issue reside in paths specific to the
316    /// issue. This function returns the part unique for the issue, e.g. the
317    /// part after the  `dit/`.
318    ///
319    pub fn ref_part(&self) -> String {
320        self.id().to_string()
321    }
322}
323
324impl<'r> fmt::Display for Issue<'r> {
325    fn fmt(&self, f: &mut fmt::Formatter) -> RResult<(), fmt::Error> {
326        write!(f, "{}", self.id())
327    }
328}
329
330impl<'r> PartialEq for Issue<'r> {
331    fn eq(&self, other: &Self) -> bool {
332        self.id() == other.id()
333    }
334}
335
336impl<'r> Eq for Issue<'r> {}
337
338impl<'r> hash::Hash for Issue<'r> {
339    fn hash<H>(&self, state: &mut H)
340        where H: hash::Hasher
341    {
342        self.id().hash(state);
343    }
344}
345
346
347
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use test_utils::TestingRepo;
353
354    use repository::RepositoryExt;
355
356    // IssueRefType tests
357
358    #[test]
359    fn ref_identification() {
360        {
361            let (id, reftype) = IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/head")
362                .expect("Expected valid issue id and ref type");
363            assert_eq!(id.to_string(), "65b56706fdc3501749d008750c61a1f24b888f72");
364            assert_eq!(reftype, IssueRefType::Head);
365        }
366        {
367            let (id, reftype) = IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/leaves/f6bd121bdc2ba5906e412da19191a2eaf2025755")
368                .expect("Expected valid issue id and ref type");
369            assert_eq!(id.to_string(), "65b56706fdc3501749d008750c61a1f24b888f72");
370            assert_eq!(reftype, IssueRefType::Leaf);
371        }
372
373        assert!(IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/foo/f6bd121bdc2ba5906e412da19191a2eaf2025755").is_none());
374        assert!(IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/head/foo").is_none());
375        assert!(IssueRefType::of_ref("refs/dit/65b56706fdc3501749d008750c61a1f24b888f72/leaves/foo").is_none());
376        assert!(IssueRefType::of_ref("refs/dit/foo/leaves/f6bd121bdc2ba5906e412da19191a2eaf2025755").is_none());
377        assert!(IssueRefType::of_ref("refs/dit/foo/head").is_none());
378        assert!(IssueRefType::of_ref("refs/foo/65b56706fdc3501749d008750c61a1f24b888f72/head").is_none());
379        assert!(IssueRefType::of_ref("refs/foo/65b56706fdc3501749d008750c61a1f24b888f72/leaves/f6bd121bdc2ba5906e412da19191a2eaf2025755").is_none());
380    }
381
382    // Issue tests
383
384    #[test]
385    fn issue_leaves() {
386        let mut testing_repo = TestingRepo::new("issue_leaves");
387        let repo = testing_repo.repo();
388
389        let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
390            .expect("Could not create signature");
391        let empty_tree = repo
392            .empty_tree()
393            .expect("Could not create empty tree");
394
395        {
396            // messages we're not supposed to see
397            let issue = repo
398                .create_issue(&sig, &sig, "Test message 1", &empty_tree, vec![])
399                .expect("Could not create issue");
400            let initial_message = issue
401                .initial_message()
402                .expect("Could not retrieve initial message");
403            issue.add_message(&sig, &sig, "Test message 2", &empty_tree, vec![&initial_message])
404                .expect("Could not add message");
405        }
406
407        let issue = repo
408            .create_issue(&sig, &sig, "Test message 3", &empty_tree, vec![])
409            .expect("Could not create issue");
410        let initial_message = issue
411            .initial_message()
412            .expect("Could not retrieve initial message");
413        let message = issue
414            .add_message(&sig, &sig, "Test message 4", &empty_tree, vec![&initial_message])
415            .expect("Could not add message");
416
417        let mut leaves = issue
418            .local_refs(IssueRefType::Leaf)
419            .expect("Could not retrieve issue leaves");
420        let leaf = leaves
421            .next()
422            .expect("Could not find leaf reference")
423            .expect("Could not retrieve leaf reference")
424            .target()
425            .expect("Could not determine the target of the leaf reference");
426        assert_eq!(leaf, message.id());
427        assert!(leaves.next().is_none());
428    }
429
430    #[test]
431    fn local_refs() {
432        let mut testing_repo = TestingRepo::new("local_refs");
433        let repo = testing_repo.repo();
434
435        let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
436            .expect("Could not create signature");
437        let empty_tree = repo
438            .empty_tree()
439            .expect("Could not create empty tree");
440
441        {
442            // messages we're not supposed to see
443            let issue = repo
444                .create_issue(&sig, &sig, "Test message 1", &empty_tree, vec![])
445                .expect("Could not create issue");
446            let initial_message = issue
447                .initial_message()
448                .expect("Could not retrieve initial message");
449            issue.add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message])
450                .expect("Could not add message");
451        }
452
453        let issue = repo
454            .create_issue(&sig, &sig, "Test message 2", &empty_tree, vec![])
455            .expect("Could not create issue");
456        let initial_message = issue
457            .initial_message()
458            .expect("Could not retrieve initial message");
459        let message = issue
460            .add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message])
461            .expect("Could not add message");
462
463        let mut ids = vec![issue.id(), message.id()];
464        ids.sort();
465        let mut ref_ids: Vec<Oid> = issue
466            .local_refs(IssueRefType::Any)
467            .expect("Could not retrieve local refs")
468            .map(|reference| reference.unwrap().target().unwrap())
469            .collect();
470        ref_ids.sort();
471        assert_eq!(ref_ids, ids);
472    }
473
474    #[test]
475    fn message_revwalk() {
476        let mut testing_repo = TestingRepo::new("message_revwalk");
477        let repo = testing_repo.repo();
478
479        let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
480            .expect("Could not create signature");
481        let empty_tree = repo
482            .empty_tree()
483            .expect("Could not create empty tree");
484
485        let issue1 = repo
486            .create_issue(&sig, &sig, "Test message 1", &empty_tree, vec![])
487            .expect("Could not create issue");
488        let initial_message1 = issue1
489            .initial_message()
490            .expect("Could not retrieve initial message");
491
492        let issue2 = repo
493            .create_issue(&sig, &sig, "Test message 2", &empty_tree, vec![&initial_message1])
494            .expect("Could not create issue");
495        let initial_message2 = issue2
496            .initial_message()
497            .expect("Could not retrieve initial message");
498        let message = issue2
499            .add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message2])
500            .expect("Could not add message");
501        let message_id = message.id();
502
503        let mut iter1 = issue1
504            .messages()
505            .expect("Could not create message revwalk iterator");
506        assert_eq!(iter1.next().unwrap().unwrap().id(), issue1.id());
507        assert!(iter1.next().is_none());
508
509        let mut iter2 = issue2
510            .messages()
511            .expect("Could not create message revwalk iterator");
512        assert_eq!(iter2.next().unwrap().unwrap().id(), message_id);
513        assert_eq!(iter2.next().unwrap().unwrap().id(), issue2.id());
514        assert!(iter2.next().is_none());
515    }
516
517    #[test]
518    fn update_head() {
519        let mut testing_repo = TestingRepo::new("update_head");
520        let repo = testing_repo.repo();
521
522        let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
523            .expect("Could not create signature");
524        let empty_tree = repo
525            .empty_tree()
526            .expect("Could not create empty tree");
527
528        let issue = repo
529            .create_issue(&sig, &sig, "Test message 2", &empty_tree, vec![])
530            .expect("Could not create issue");
531        let initial_message = issue
532            .initial_message()
533            .expect("Could not retrieve initial message");
534        let message = issue
535            .add_message(&sig, &sig, "Test message 3", &empty_tree, vec![&initial_message])
536            .expect("Could not add message");
537
538        assert_eq!(issue.local_head().unwrap().target().unwrap(), issue.id());
539
540        issue
541            .update_head(message.id(), true)
542            .expect("Could not update head reference");
543        assert_eq!(issue.local_head().unwrap().target().unwrap(), message.id());
544    }
545}
546