1
use crate::git_signatures::{check_signatures, get_signatures};
2
use git2::Repository;
3
use sequoia_openpgp::Cert;
4
use serde::Serialize;
5
use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
6

            
7
type Result<T> = anyhow::Result<T>;
8

            
9
6
#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
10
pub struct AuthorData {
11
    names: BTreeSet<String>,
12
    number_of_commits: usize,
13
}
14

            
15
type Email = String;
16

            
17
4
#[derive(Serialize, PartialEq)]
18
pub struct Commit {
19
    time: i64,
20
    sha1: String,
21
    email: Option<Email>,
22
    name: Option<String>,
23
}
24

            
25
2
#[derive(Serialize, PartialEq)]
26
pub struct RepositoryAnalysis {
27
    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
28
    signatures: BTreeSet<SignatureAnalysis>,
29
    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
30
    missing_keys: BTreeSet<String>,
31
    default_branch: Option<String>,
32
    first_commit: Option<Commit>,
33
    latest_commit: Option<Commit>,
34
    number_of_commits: usize,
35
    authors: BTreeMap<Email, AuthorData>,
36
}
37

            
38
#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
39
pub struct SignatureAnalysis {
40
    handle: String,
41
    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
42
    email: BTreeSet<String>,
43
}
44

            
45
820
fn add_author(commit: &git2::Commit, authors: &mut BTreeMap<Email, AuthorData>) {
46
820
    let author = commit.author();
47
820
    if let (Some(name), Some(email)) = (author.name(), author.email()) {
48
820
        let email = email.to_string();
49
820
        let name = name.to_string();
50
820
        match authors.entry(email) {
51
790
            Entry::Occupied(mut e) => {
52
790
                let e = e.get_mut();
53
790
                e.number_of_commits += 1;
54
790
                e.names.insert(name);
55
790
            }
56
30
            Entry::Vacant(e) => {
57
30
                let mut names = BTreeSet::new();
58
30
                names.insert(name);
59
30
                e.insert(AuthorData {
60
30
                    number_of_commits: 1,
61
30
                    names,
62
30
                });
63
30
            }
64
        }
65
    }
66
820
}
67

            
68
20
fn get_commit(commit: &git2::Commit) -> Commit {
69
20
    let author = commit.author();
70
20
    Commit {
71
20
        time: commit.time().seconds(),
72
20
        sha1: commit.id().to_string(),
73
20
        email: author.email().map(|e| e.to_string()),
74
20
        name: author.name().map(|e| e.to_string()),
75
20
    }
76
20
}
77

            
78
10
pub fn analyze_repository(certs: &[Cert], repo: &Repository) -> Result<RepositoryAnalysis> {
79
10
    let signatures = get_signatures(repo)?;
80
10
    let r = check_signatures(certs, signatures)?;
81
10
    let mut signature_analyses = BTreeSet::new();
82
10
    let mut authors = BTreeMap::new();
83
10
    for key in r.found_keys {
84
        let mut analysis = SignatureAnalysis {
85
            handle: format!("{}", key.0),
86
            email: BTreeSet::new(),
87
        };
88
        for id in key.1.userids() {
89
            if let Ok(Some(email)) = id.email_normalized() {
90
                analysis.email.insert(email);
91
            }
92
        }
93
        signature_analyses.insert(analysis);
94
    }
95
    // since the checkout is untouched, head is the default branch
96
10
    let mut default_branch = None;
97
10
    let mut first_commit = None;
98
10
    let mut latest_commit = None;
99
10
    let mut number_of_commits = 0;
100
10
    if let Ok(head) = repo.head() {
101
10
        default_branch = head
102
10
            .name()
103
10
            .and_then(|n| n.strip_prefix("refs/heads/"))
104
10
            .map(|n| n.to_string());
105
10
        if let Ok(mut commit) = head.peel_to_commit() {
106
10
            number_of_commits = 1;
107
10
            latest_commit = Some(get_commit(&commit));
108
10
            add_author(&commit, &mut authors);
109
820
            while let Some(p) = commit.parents().next() {
110
810
                number_of_commits += 1;
111
810
                commit = p;
112
810
                add_author(&commit, &mut authors);
113
810
            }
114
10
            first_commit = Some(get_commit(&commit));
115
        }
116
    }
117
    //repo.head().
118
10
    Ok(RepositoryAnalysis {
119
10
        signatures: signature_analyses,
120
20
        missing_keys: r.missing_keys.iter().map(|k| format!("{}", k)).collect(),
121
10
        default_branch,
122
10
        first_commit,
123
10
        latest_commit,
124
10
        number_of_commits,
125
10
        authors,
126
10
    })
127
10
}