libgitdit/
iter.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//! Utility iterators
11//!
12//! This module provides various iterators.
13//!
14
15use git2::{self, Repository};
16use std::borrow::Borrow;
17use std::collections::HashMap;
18use std::iter::FromIterator;
19
20use issue;
21use repository::RepositoryExt;
22use trailer::{accumulation, spec};
23
24use error::*;
25use error::ErrorKind as EK;
26
27/// Iterator for transforming the names of head references to issues
28///
29/// This iterator wrapps a `ReferenceNames` iterator and returns issues
30/// associated to the head references returned by the wrapped iterator.
31///
32pub struct HeadRefsToIssuesIter<'r>
33{
34    inner: git2::References<'r>,
35    repo: &'r Repository
36}
37
38impl<'r> HeadRefsToIssuesIter<'r>
39{
40    pub fn new(repo: &'r Repository, inner: git2::References<'r>) -> Self {
41        HeadRefsToIssuesIter { inner: inner, repo: repo }
42    }
43}
44
45impl<'r> Iterator for HeadRefsToIssuesIter<'r>
46{
47    type Item = Result<issue::Issue<'r>>;
48
49    fn next(&mut self) -> Option<Self::Item> {
50        self.inner
51            .next()
52            .map(|reference| {
53                reference
54                    .chain_err(|| EK::CannotGetReference)
55                    .and_then(|r| self.repo.issue_by_head_ref(&r))
56            })
57    }
58}
59
60
61/// Messages iter
62///
63/// Use this iterator if you intend to iterate over messages rather than `Oid`s
64/// via a `Revwalk`.
65///
66pub struct Messages<'r> {
67    pub(crate) revwalk: git2::Revwalk<'r>,
68    repo: &'r Repository,
69}
70
71impl<'r> Messages<'r> {
72    /// Create a new Messages itrator from a revwalk for a given repo
73    ///
74    pub fn new<'a>(repo: &'a Repository, revwalk: git2::Revwalk<'a>) -> Messages<'a> {
75        Messages { revwalk: revwalk, repo: repo }
76    }
77
78    /// Create a new messages iter from an unconfigured revwalk
79    ///
80    pub fn empty<'a>(repo: &'a Repository) -> Result<Messages<'a>> {
81        repo.revwalk()
82            .map(|revwalk| Self::new(repo, revwalk))
83            .chain_err(|| EK::CannotConstructRevwalk)
84    }
85
86    /// Create an IssueMessagesIter from this instance
87    ///
88    pub fn until_any_initial(self) -> IssueMessagesIter<'r> {
89        self.into()
90    }
91
92    /// Terminate this iterator at the given issue's initial message
93    ///
94    /// This method hides the initial message's parents. It is somewhat more
95    /// performant than creating an `IssueMessagesIter`. However, the issue has
96    /// to be known in advance.
97    ///
98    pub fn terminate_at_initial(&mut self, issue: &issue::Issue) -> Result<()> {
99        for parent in issue.initial_message()?.parent_ids() {
100            self.revwalk.hide(parent)?;
101        }
102        Ok(())
103    }
104}
105
106impl<'r> Iterator for Messages<'r> {
107    type Item = Result<git2::Commit<'r>>;
108
109    fn next(&mut self) -> Option<Self::Item> {
110        self.revwalk
111            .next()
112            .map(|item| item
113                .and_then(|id| self.repo.find_commit(id))
114                .chain_err(|| EK::CannotGetCommit)
115            )
116    }
117}
118
119
120/// Messages iterator extension trait
121///
122/// This trait provides some convenience functionality for iterators over
123/// `Message`s which does not need to be part of `Messages` or another iterator.
124///
125pub trait MessagesExt {
126    type Output:
127        accumulation::MultiAccumulator +
128        FromIterator<(String, accumulation::ValueAccumulator)>;
129
130    /// Accumulate trailers according to the specification provided
131    ///
132    /// This function accumulates all specified trailers from the messages
133    /// returned by the iterator.
134    ///
135    fn accumulate_trailers<'a, I, J>(self, specs: I) -> Self::Output
136        where I: IntoIterator<Item = J>,
137              J: Borrow<spec::TrailerSpec<'a>>;
138}
139
140impl<'a, I> MessagesExt for I
141    where I: Iterator<Item = git2::Commit<'a>>
142{
143    type Output = HashMap<String, accumulation::ValueAccumulator>;
144
145    fn accumulate_trailers<'b, J, K>(self, specs: J) -> Self::Output
146        where J: IntoIterator<Item = K>,
147              K: Borrow<spec::TrailerSpec<'b>>
148    {
149        use message::Message;
150        use trailer::accumulation::Accumulator;
151        use trailer::spec::ToMap;
152
153        let mut accumulator = specs.into_map();
154        accumulator.process_all(self.flat_map(|message| message.trailers()));
155        accumulator
156    }
157}
158
159
160/// Iterator iterating over messages of an issue
161///
162/// This iterator returns the first parent of a commit or message successively
163/// until an initial issue message is encountered, inclusively.
164///
165pub struct IssueMessagesIter<'r>(Messages<'r>);
166
167impl<'r> IssueMessagesIter<'r> {
168    /// Fuse the iterator is the id refers to an issue
169    ///
170    fn fuse_if_initial(&mut self, id: git2::Oid) {
171        if self.0.repo.find_issue(id).is_ok() {
172            self.0.revwalk.reset();
173        }
174    }
175}
176
177impl<'r> From<Messages<'r>> for IssueMessagesIter<'r> {
178    fn from(messages: Messages<'r>) -> Self {
179        IssueMessagesIter(messages)
180    }
181}
182
183impl<'r> Iterator for IssueMessagesIter<'r> {
184    type Item = Result<git2::Commit<'r>>;
185
186    fn next(&mut self) -> Option<Self::Item> {
187        self.0
188            .next()
189            .map(|item| {
190                if let Ok(ref commit) = item {
191                    self.fuse_if_initial(commit.id());
192                }
193                item
194            })
195    }
196}
197
198
199/// Iterator over references referring to any of a number of commits
200///
201/// This iterator wraps a `git2::Revwalk`. It will iterate over the commits
202/// provided by the wrapped iterator. If one of those commits is referred to
203/// by any of the whatched references, that references will be returned.
204///
205/// Only "watched" references are returned, e.g. they need to be supplied
206/// through the `watch_ref()` function. Each reference will only be returned
207/// once.
208///
209pub struct RefsReferringTo<'r> {
210    refs: HashMap<git2::Oid, Vec<git2::Reference<'r>>>,
211    inner: git2::Revwalk<'r>,
212    current_refs: Vec<git2::Reference<'r>>,
213}
214
215impl<'r> RefsReferringTo<'r> {
216    /// Create a new iterator iterating over the messages supplied
217    ///
218    pub fn new(messages: git2::Revwalk<'r>) -> Self
219    {
220        Self { refs: HashMap::new(), inner: messages, current_refs: Vec::new() }
221    }
222
223    /// Push a starting point for the iteration
224    ///
225    /// The message will be pushed onto the underlying `Revwalk` used for
226    /// iterating over messages.
227    ///
228    pub fn push(&mut self, message: git2::Oid) -> Result<()> {
229        self.inner.push(message).chain_err(|| EK::CannotConstructRevwalk)
230    }
231
232    /// Start watching a reference
233    ///
234    /// A watched reference may be returned by the iterator.
235    ///
236    pub fn watch_ref(&mut self, reference: git2::Reference<'r>) -> Result<()> {
237        let id = reference
238            .peel(git2::ObjectType::Any)
239            .chain_err(|| EK::CannotGetCommitForRev(reference.name().unwrap_or_default().to_string()))?
240            .id();
241        self.refs.entry(id).or_insert_with(Vec::new).push(reference);
242        Ok(())
243    }
244
245    /// Start watching a number of references
246    ///
247    pub fn watch_refs<I>(&mut self, references: I) -> Result<()>
248        where I: IntoIterator<Item = git2::Reference<'r>>
249    {
250        for reference in references.into_iter() {
251            self.watch_ref(reference)?;
252        }
253        Ok(())
254    }
255}
256
257impl<'r> Iterator for RefsReferringTo<'r> {
258    type Item = Result<git2::Reference<'r>>;
259
260    fn next(&mut self) -> Option<Self::Item> {
261        'outer: loop {
262            if let Some(reference) = self.current_refs.pop() {
263                // get one of the references for the current commit
264                return Some(Ok(reference));
265            }
266
267            // Refills may be rather expensive. Let's check whether we have any
268            // refs left, first.
269            if self.refs.is_empty() {
270                return None;
271            }
272
273            // refill the stash of references for the next commit
274            for item in &mut self.inner {
275                match item.chain_err(|| EK::CannotGetCommit) {
276                    Ok(id) => if let Some(new_refs) = self.refs.remove(&id) {
277                        // NOTE: should new_refs be empty, we just loop once
278                        //       more through the 'outer loop
279                        self.current_refs = new_refs;
280                        continue 'outer;
281                    },
282                    Err(err) => return Some(Err(err)),
283                }
284            }
285
286            // We depleted the inner iterator.
287            return None;
288        }
289    }
290}
291
292
293/// Implementation of Extend for RefsReferringTo
294///
295/// The references supplied will be returned by the extended `RefsReferringTo`
296/// iterator.
297///
298impl<'r> Extend<git2::Reference<'r>> for RefsReferringTo<'r> {
299    fn extend<I>(&mut self, references: I)
300        where I: IntoIterator<Item = git2::Reference<'r>>
301    {
302        self.current_refs.extend(references);
303    }
304}
305
306
307/// Iterator for deleting references
308///
309/// This iterator wraps an iterator over references. All of the references
310/// returned by the wrapped iterator are deleted. The `ReferenceDeletingIter`
311/// itself returns (only) the errors encountered. Sucessful deletions are not
312/// reported, e.g. no items will be returned.
313///
314/// Use this iterator if you want to remove references from a repository but
315/// also want to delegate the decision what to do if an error is encountered.
316///
317pub struct ReferenceDeletingIter<'r, I>
318    where I: Iterator<Item = git2::Reference<'r>>
319{
320    inner: I
321}
322
323impl<'r, I> ReferenceDeletingIter<'r, I>
324    where I: Iterator<Item = git2::Reference<'r>>
325{
326    /// Delete, ignoring errors
327    ///
328    /// Delete all references returned by the wrapped iterator, ignoring all
329    /// errors.
330    ///
331    pub fn delete_ignoring(self) {
332        for _ in self {}
333    }
334}
335
336impl<'r, I, J> From<J> for ReferenceDeletingIter<'r, I>
337    where I: Iterator<Item = git2::Reference<'r>>,
338          J: IntoIterator<Item = git2::Reference<'r>, IntoIter = I>
339{
340    fn from(items: J) -> Self {
341        ReferenceDeletingIter { inner: items.into_iter() }
342    }
343}
344
345impl<'r, I> Iterator for ReferenceDeletingIter<'r, I>
346    where I: Iterator<Item = git2::Reference<'r>>
347{
348    type Item = Error;
349
350    fn next(&mut self) -> Option<Self::Item> {
351        self.inner
352            .by_ref()
353            .filter_map(|mut r| r
354                .delete()
355                .chain_err(|| EK::CannotDeleteReference(r.name().unwrap_or_default().to_string()))
356                .err()
357            )
358            .next()
359    }
360}
361
362
363
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use test_utils::TestingRepo;
369
370    use repository::RepositoryExt;
371
372    // RefsReferringTo tests
373
374    #[test]
375    fn referred_refs() {
376        let mut testing_repo = TestingRepo::new("referred_refs");
377        let repo = testing_repo.repo();
378
379        let sig = git2::Signature::now("Foo Bar", "foo.bar@example.com")
380            .expect("Could not create signature");
381        let empty_tree = repo
382            .empty_tree()
383            .expect("Could not create empty tree");
384        let empty_parents: Vec<&git2::Commit> = vec![];
385
386        let mut commits = repo.revwalk().expect("Could not create revwalk");
387        let mut refs_to_watch = Vec::new();
388        let mut refs_to_report = Vec::new();
389
390        {
391            let commit = repo
392                .commit(None, &sig, &sig, "Test message 1", &empty_tree, &empty_parents)
393                .expect("Could not create commit");
394            let refa = repo
395                .reference("refs/test/1a", commit, false, "create test ref 1a")
396                .expect("Could not create reference");
397            let refb = repo
398                .reference("refs/test/1b", commit, false, "create test ref 1b")
399                .expect("Could not create reference");
400            commits.push(commit).expect("Could not push commit onto revwalk");
401            refs_to_report.push(refa.name().expect("Could not retrieve name").to_string());
402            refs_to_report.push(refb.name().expect("Could not retrieve name").to_string());
403            refs_to_watch.push(refa);
404            refs_to_watch.push(refb);
405        }
406
407        {
408            let commit = repo
409                .commit(None, &sig, &sig, "Test message 2", &empty_tree, &empty_parents)
410                .expect("Could not create commit");
411            let refa = repo
412                .reference("refs/test/2a", commit, false, "create test ref 2a")
413                .expect("Could not create reference");
414            repo.reference("refs/test/2b", commit, false, "create test ref 2b")
415                .expect("Could not create reference");
416            commits.push(commit).expect("Could not push commit onto revwalk");
417            refs_to_report.push(refa.name().expect("Could not retrieve name").to_string());
418            refs_to_watch.push(refa);
419        }
420
421        {
422            let commit = repo
423                .commit(None, &sig, &sig, "Test message 3", &empty_tree, &empty_parents)
424                .expect("Could not create commit");
425            repo.reference("refs/test/3a", commit, false, "create test ref 3a")
426                .expect("Could not create reference");
427            repo.reference("refs/test/3b", commit, false, "create test ref 3b")
428                .expect("Could not create reference");
429            commits.push(commit).expect("Could not push commit onto revwalk");
430        }
431
432        {
433            let commit = repo
434                .commit(None, &sig, &sig, "Test message 4", &empty_tree, &empty_parents)
435                .expect("Could not create commit");
436            let refa = repo
437                .reference("refs/test/4a", commit, false, "create test ref 4a")
438                .expect("Could not create reference");
439            let refb = repo
440                .reference("refs/test/4b", commit, false, "create test ref 4b")
441                .expect("Could not create reference");
442            refs_to_watch.push(refa);
443            refs_to_watch.push(refb);
444        }
445
446        let mut referred = RefsReferringTo::new(commits);
447        referred.watch_refs(refs_to_watch).expect("Could not watch refs");
448
449        let mut reported: Vec<_> = referred
450            .map(|item| item
451                .expect("Error during iterating over refs")
452                .name()
453                .expect("Could not retrieve name")
454                .to_string()
455            )
456            .collect();
457        reported.sort();
458        refs_to_report.sort();
459        assert_eq!(reported, refs_to_report);
460    }
461}
462