libgitdit 0.5.0

A library for distributed issue tracking in git
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
// git-dit - the distributed issue tracker for git
// Copyright (C) 2016, 2017 Matthias Beyer <mail@beyermatthias.de>
// Copyright (C) 2016, 2017 Julian Ganz <neither@nut.email>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//

//! Utility iterators
//!
//! This module provides various iterators.
//!

use git2::{self, Repository};
use std::borrow::Borrow;
use std::collections::HashMap;
use std::iter::FromIterator;

use issue;
use repository::RepositoryExt;
use trailer::{accumulation, spec};

use error::*;
use error::ErrorKind as EK;

/// Iterator for transforming the names of head references to issues
///
/// This iterator wrapps a `ReferenceNames` iterator and returns issues
/// associated to the head references returned by the wrapped iterator.
///
pub struct HeadRefsToIssuesIter<'r>
{
    inner: git2::References<'r>,
    repo: &'r Repository
}

impl<'r> HeadRefsToIssuesIter<'r>
{
    pub fn new(repo: &'r Repository, inner: git2::References<'r>) -> Self {
        HeadRefsToIssuesIter { inner: inner, repo: repo }
    }
}

impl<'r> Iterator for HeadRefsToIssuesIter<'r>
{
    type Item = Result<issue::Issue<'r>>;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner
            .next()
            .map(|reference| {
                reference
                    .chain_err(|| EK::CannotGetReference)
                    .and_then(|r| self.repo.issue_by_head_ref(&r))
            })
    }
}


/// Messages iter
///
/// Use this iterator if you intend to iterate over messages rather than `Oid`s
/// via a `Revwalk`.
///
pub struct Messages<'r> {
    pub(crate) revwalk: git2::Revwalk<'r>,
    repo: &'r Repository,
}

impl<'r> Messages<'r> {
    /// Create a new Messages itrator from a revwalk for a given repo
    ///
    pub fn new<'a>(repo: &'a Repository, revwalk: git2::Revwalk<'a>) -> Messages<'a> {
        Messages { revwalk: revwalk, repo: repo }
    }

    /// Create a new messages iter from an unconfigured revwalk
    ///
    pub fn empty<'a>(repo: &'a Repository) -> Result<Messages<'a>> {
        repo.revwalk()
            .map(|revwalk| Self::new(repo, revwalk))
            .chain_err(|| EK::CannotConstructRevwalk)
    }

    /// Create an IssueMessagesIter from this instance
    ///
    pub fn until_any_initial(self) -> IssueMessagesIter<'r> {
        self.into()
    }

    /// Terminate this iterator at the given issue's initial message
    ///
    /// This method hides the initial message's parents. It is somewhat more
    /// performant than creating an `IssueMessagesIter`. However, the issue has
    /// to be known in advance.
    ///
    pub fn terminate_at_initial(&mut self, issue: &issue::Issue) -> Result<()> {
        for parent in issue.initial_message()?.parent_ids() {
            self.revwalk.hide(parent)?;
        }
        Ok(())
    }
}

impl<'r> Iterator for Messages<'r> {
    type Item = Result<git2::Commit<'r>>;

    fn next(&mut self) -> Option<Self::Item> {
        self.revwalk
            .next()
            .map(|item| item
                .and_then(|id| self.repo.find_commit(id))
                .chain_err(|| EK::CannotGetCommit)
            )
    }
}


/// Messages iterator extension trait
///
/// This trait provides some convenience functionality for iterators over
/// `Message`s which does not need to be part of `Messages` or another iterator.
///
pub trait MessagesExt {
    type Output:
        accumulation::MultiAccumulator +
        FromIterator<(String, accumulation::ValueAccumulator)>;

    /// Accumulate trailers according to the specification provided
    ///
    /// This function accumulates all specified trailers from the messages
    /// returned by the iterator.
    ///
    fn accumulate_trailers<'a, I, J>(self, specs: I) -> Self::Output
        where I: IntoIterator<Item = J>,
              J: Borrow<spec::TrailerSpec<'a>>;
}

impl<'a, I> MessagesExt for I
    where I: Iterator<Item = git2::Commit<'a>>
{
    type Output = HashMap<String, accumulation::ValueAccumulator>;

    fn accumulate_trailers<'b, J, K>(self, specs: J) -> Self::Output
        where J: IntoIterator<Item = K>,
              K: Borrow<spec::TrailerSpec<'b>>
    {
        use message::Message;
        use trailer::accumulation::Accumulator;
        use trailer::spec::ToMap;

        let mut accumulator = specs.into_map();
        accumulator.process_all(self.flat_map(|message| message.trailers()));
        accumulator
    }
}


/// Iterator iterating over messages of an issue
///
/// This iterator returns the first parent of a commit or message successively
/// until an initial issue message is encountered, inclusively.
///
pub struct IssueMessagesIter<'r>(Messages<'r>);

impl<'r> IssueMessagesIter<'r> {
    /// Fuse the iterator is the id refers to an issue
    ///
    fn fuse_if_initial(&mut self, id: git2::Oid) {
        if self.0.repo.find_issue(id).is_ok() {
            self.0.revwalk.reset();
        }
    }
}

impl<'r> From<Messages<'r>> for IssueMessagesIter<'r> {
    fn from(messages: Messages<'r>) -> Self {
        IssueMessagesIter(messages)
    }
}

impl<'r> Iterator for IssueMessagesIter<'r> {
    type Item = Result<git2::Commit<'r>>;

    fn next(&mut self) -> Option<Self::Item> {
        self.0
            .next()
            .map(|item| {
                if let Ok(ref commit) = item {
                    self.fuse_if_initial(commit.id());
                }
                item
            })
    }
}


/// Iterator over references referring to any of a number of commits
///
/// This iterator wraps a `git2::Revwalk`. It will iterate over the commits
/// provided by the wrapped iterator. If one of those commits is referred to
/// by any of the whatched references, that references will be returned.
///
/// Only "watched" references are returned, e.g. they need to be supplied
/// through the `watch_ref()` function. Each reference will only be returned
/// once.
///
pub struct RefsReferringTo<'r> {
    refs: HashMap<git2::Oid, Vec<git2::Reference<'r>>>,
    inner: git2::Revwalk<'r>,
    current_refs: Vec<git2::Reference<'r>>,
}

impl<'r> RefsReferringTo<'r> {
    /// Create a new iterator iterating over the messages supplied
    ///
    pub fn new(messages: git2::Revwalk<'r>) -> Self
    {
        Self { refs: HashMap::new(), inner: messages, current_refs: Vec::new() }
    }

    /// Push a starting point for the iteration
    ///
    /// The message will be pushed onto the underlying `Revwalk` used for
    /// iterating over messages.
    ///
    pub fn push(&mut self, message: git2::Oid) -> Result<()> {
        self.inner.push(message).chain_err(|| EK::CannotConstructRevwalk)
    }

    /// Start watching a reference
    ///
    /// A watched reference may be returned by the iterator.
    ///
    pub fn watch_ref(&mut self, reference: git2::Reference<'r>) -> Result<()> {
        let id = reference
            .peel(git2::ObjectType::Any)
            .chain_err(|| EK::CannotGetCommitForRev(reference.name().unwrap_or_default().to_string()))?
            .id();
        self.refs.entry(id).or_insert_with(Vec::new).push(reference);
        Ok(())
    }

    /// Start watching a number of references
    ///
    pub fn watch_refs<I>(&mut self, references: I) -> Result<()>
        where I: IntoIterator<Item = git2::Reference<'r>>
    {
        for reference in references.into_iter() {
            self.watch_ref(reference)?;
        }
        Ok(())
    }
}

impl<'r> Iterator for RefsReferringTo<'r> {
    type Item = Result<git2::Reference<'r>>;

    fn next(&mut self) -> Option<Self::Item> {
        'outer: loop {
            if let Some(reference) = self.current_refs.pop() {
                // get one of the references for the current commit
                return Some(Ok(reference));
            }

            // Refills may be rather expensive. Let's check whether we have any
            // refs left, first.
            if self.refs.is_empty() {
                return None;
            }

            // refill the stash of references for the next commit
            for item in &mut self.inner {
                match item.chain_err(|| EK::CannotGetCommit) {
                    Ok(id) => if let Some(new_refs) = self.refs.remove(&id) {
                        // NOTE: should new_refs be empty, we just loop once
                        //       more through the 'outer loop
                        self.current_refs = new_refs;
                        continue 'outer;
                    },
                    Err(err) => return Some(Err(err)),
                }
            }

            // We depleted the inner iterator.
            return None;
        }
    }
}


/// Implementation of Extend for RefsReferringTo
///
/// The references supplied will be returned by the extended `RefsReferringTo`
/// iterator.
///
impl<'r> Extend<git2::Reference<'r>> for RefsReferringTo<'r> {
    fn extend<I>(&mut self, references: I)
        where I: IntoIterator<Item = git2::Reference<'r>>
    {
        self.current_refs.extend(references);
    }
}


/// Iterator for deleting references
///
/// This iterator wraps an iterator over references. All of the references
/// returned by the wrapped iterator are deleted. The `ReferenceDeletingIter`
/// itself returns (only) the errors encountered. Sucessful deletions are not
/// reported, e.g. no items will be returned.
///
/// Use this iterator if you want to remove references from a repository but
/// also want to delegate the decision what to do if an error is encountered.
///
pub struct ReferenceDeletingIter<'r, I>
    where I: Iterator<Item = git2::Reference<'r>>
{
    inner: I
}

impl<'r, I> ReferenceDeletingIter<'r, I>
    where I: Iterator<Item = git2::Reference<'r>>
{
    /// Delete, ignoring errors
    ///
    /// Delete all references returned by the wrapped iterator, ignoring all
    /// errors.
    ///
    pub fn delete_ignoring(self) {
        for _ in self {}
    }
}

impl<'r, I, J> From<J> for ReferenceDeletingIter<'r, I>
    where I: Iterator<Item = git2::Reference<'r>>,
          J: IntoIterator<Item = git2::Reference<'r>, IntoIter = I>
{
    fn from(items: J) -> Self {
        ReferenceDeletingIter { inner: items.into_iter() }
    }
}

impl<'r, I> Iterator for ReferenceDeletingIter<'r, I>
    where I: Iterator<Item = git2::Reference<'r>>
{
    type Item = Error;

    fn next(&mut self) -> Option<Self::Item> {
        self.inner
            .by_ref()
            .filter_map(|mut r| r
                .delete()
                .chain_err(|| EK::CannotDeleteReference(r.name().unwrap_or_default().to_string()))
                .err()
            )
            .next()
    }
}




#[cfg(test)]
mod tests {
    use super::*;
    use test_utils::TestingRepo;

    use repository::RepositoryExt;

    // RefsReferringTo tests

    #[test]
    fn referred_refs() {
        let mut testing_repo = TestingRepo::new("referred_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 empty_parents: Vec<&git2::Commit> = vec![];

        let mut commits = repo.revwalk().expect("Could not create revwalk");
        let mut refs_to_watch = Vec::new();
        let mut refs_to_report = Vec::new();

        {
            let commit = repo
                .commit(None, &sig, &sig, "Test message 1", &empty_tree, &empty_parents)
                .expect("Could not create commit");
            let refa = repo
                .reference("refs/test/1a", commit, false, "create test ref 1a")
                .expect("Could not create reference");
            let refb = repo
                .reference("refs/test/1b", commit, false, "create test ref 1b")
                .expect("Could not create reference");
            commits.push(commit).expect("Could not push commit onto revwalk");
            refs_to_report.push(refa.name().expect("Could not retrieve name").to_string());
            refs_to_report.push(refb.name().expect("Could not retrieve name").to_string());
            refs_to_watch.push(refa);
            refs_to_watch.push(refb);
        }

        {
            let commit = repo
                .commit(None, &sig, &sig, "Test message 2", &empty_tree, &empty_parents)
                .expect("Could not create commit");
            let refa = repo
                .reference("refs/test/2a", commit, false, "create test ref 2a")
                .expect("Could not create reference");
            repo.reference("refs/test/2b", commit, false, "create test ref 2b")
                .expect("Could not create reference");
            commits.push(commit).expect("Could not push commit onto revwalk");
            refs_to_report.push(refa.name().expect("Could not retrieve name").to_string());
            refs_to_watch.push(refa);
        }

        {
            let commit = repo
                .commit(None, &sig, &sig, "Test message 3", &empty_tree, &empty_parents)
                .expect("Could not create commit");
            repo.reference("refs/test/3a", commit, false, "create test ref 3a")
                .expect("Could not create reference");
            repo.reference("refs/test/3b", commit, false, "create test ref 3b")
                .expect("Could not create reference");
            commits.push(commit).expect("Could not push commit onto revwalk");
        }

        {
            let commit = repo
                .commit(None, &sig, &sig, "Test message 4", &empty_tree, &empty_parents)
                .expect("Could not create commit");
            let refa = repo
                .reference("refs/test/4a", commit, false, "create test ref 4a")
                .expect("Could not create reference");
            let refb = repo
                .reference("refs/test/4b", commit, false, "create test ref 4b")
                .expect("Could not create reference");
            refs_to_watch.push(refa);
            refs_to_watch.push(refb);
        }

        let mut referred = RefsReferringTo::new(commits);
        referred.watch_refs(refs_to_watch).expect("Could not watch refs");

        let mut reported: Vec<_> = referred
            .map(|item| item
                .expect("Error during iterating over refs")
                .name()
                .expect("Could not retrieve name")
                .to_string()
            )
            .collect();
        reported.sort();
        refs_to_report.sort();
        assert_eq!(reported, refs_to_report);
    }
}