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
// Copyright 2017 Aldrin J D'Souza.
// Licensed under the MIT License <https://opensource.org/licenses/MIT>

/// Presentation and Reporting
use config;
use commit::Commit;
use config::Configuration;
use std::collections::{HashMap, HashSet};

/// The complete report
#[derive(Serialize, Debug)]
pub struct Report<'a> {
    /// Scoped changes in the report
    pub scopes: Vec<Scope>,

    /// All interesting commits
    pub commits: &'a [Commit],
}

/// A group of changes in the same scope
#[derive(Serialize, Debug)]
pub struct Scope {
    /// The title of the scope
    pub title: String,

    /// A list of categorized changes
    pub categories: Vec<Category>,
}

/// A group of changes with the same category
#[derive(Serialize, Debug)]
pub struct Category {
    /// The title of the category
    pub title: String,

    /// A list of change descriptions
    pub changes: Vec<Text>,
}

/// Change description
#[derive(Serialize, Clone, Debug)]
pub struct Text {
    /// A sequence number to inform ordering
    pub sequence: u32,

    /// An opening headline
    pub opening: String,

    /// The remaining lines in the description
    pub rest: Vec<String>,
}

/// A temporary report structure with look-ups on scope and category keys
type RawReport = HashMap<String, HashMap<String, Vec<Text>>>;

/// The running state kept during report construction
#[derive(Default, Clone, Serialize)]
struct State {
    /// The current text
    text: Vec<String>,

    /// The current scope
    scope: Option<String>,

    /// The current category
    category: Option<String>,
}

/// Generate a new report for the commits with the given configuration
pub fn generate<'a>(config: &'a Configuration, commits: &'a [Commit]) -> Report<'a> {

    // First pass - categorize
    let raw_report = first_pass(config, commits);

    // Second pass - aggregate
    let scopes = second_pass(config, &raw_report);

    // Done
    Report { commits, scopes }
}

/// The first pass - walks through commits and gathers scopes and categories.
fn first_pass(config: &Configuration, commits: &[Commit]) -> RawReport {

    // A running raw report
    let mut raw_report = RawReport::new();

    // A running counter
    let mut sequence = 0;

    // Take each commit
    for commit in commits {

        // Initialize a fresh current
        let mut current = State::default();

        // Take each line in the message
        for line in &commit.lines {

            // If this line opens a new category
            if line.category.is_some() {

                // Close out the current item
                record(&mut raw_report, config, current.clone(), &mut sequence);

                // Start a new context
                current.text = Vec::new();
                current.scope = line.scope.clone();
                current.category = line.category.clone();
            }

            // Record the line text
            current.text.push(line.text.clone().unwrap_or_default());
        }

        // Close the last open item
        record(&mut raw_report, config, current, &mut sequence);
    }

    // Log the raw_report for debugging
    debug!("RAW_REPORT: {:#?}", raw_report);

    raw_report
}

/// The second pass takes the raw report and orders things as we want to show them
fn second_pass(config: &Configuration, report: &RawReport) -> Vec<Scope> {

    // The report of all scopes
    let mut scopes = Vec::new();

    // Track the scopes we've processed (required to avoid duplicates)
    let mut processed_scopes = HashSet::new();

    // Go through each configured scope
    for scope in &config.scopes {

        // If we have changes for the scope in the report
        if let Some(categorized) = report.get(&scope.title) {

            // The scoped categorized changes
            let mut categories = Vec::new();

            // Track the categorizes we've processed (required to avoid duplicates)
            let mut processed_categories = HashSet::new();

            // If we've already seen this scope title
            if processed_scopes.contains(&scope.title) {

                // Skip it.
                continue;
            }

            // Remember this scope title as processed
            processed_scopes.insert(&scope.title);

            // Go through all configured scopes
            for category in &config.categories {

                // If we've already processed this category title
                if processed_categories.contains(&category.title) {

                    // Skip it.
                    continue;
                }

                // Remember this category title as processed
                processed_categories.insert(&category.title);

                // If there are changes of this category
                if let Some(changes) = categorized.get(&category.title) {

                    // The category title
                    let title = category.title.clone();

                    // Clone and sort to sequence text in time order
                    let mut changes = changes.clone();
                    changes.sort_by(|a, b| a.sequence.cmp(&b.sequence));

                    // Add them to the running list
                    categories.push(Category { title, changes });
                }
            }

            // Record them in the scopes list
            scopes.push(Scope {
                title: scope.title.clone(),
                categories,
            });
        }
    }

    // Done
    scopes
}

/// Record the current state into the raw report
fn record(raw: &mut RawReport, config: &Configuration, mut state: State, seq: &mut u32) {

    // Validate the scope with the configuration
    let scope = config::report_title(&config.scopes, &state.scope);

    // Validate the category with the configuration
    let category = config::report_title(&config.categories, &state.category);

    // If the scope and category are known and we have some text to record
    if category.is_some() && scope.is_some() && !state.text.is_empty() {

        // Split the opening line and the remainder
        let mut opening = state.text.remove(0);

        // If the opening line is empty,
        while opening.trim().is_empty() {

            // take the next one
            opening = state.text.remove(0)
        }

        // If the opening line has no buffer space,
        if !opening.starts_with(' ') {

            // add it
            opening.insert(0, ' ');
        }

        // Take the rest
        let rest = state.text;

        // Increment the sequence
        *seq += 1;

        // Record it in the raw report
        raw.entry(scope.unwrap())
            .or_insert_with(HashMap::new)
            .entry(category.unwrap())
            .or_insert_with(Vec::new)
            .push(Text {
                sequence: *seq,
                opening,
                rest,
            });
    }
}