Skip to main content

git_cliff_core/
release.rs

1use std::collections::HashMap;
2
3use next_version::{NextVersion as NextVersionTrait, VersionUpdater};
4use semver::Version;
5use serde::{Deserialize, Serialize};
6use serde_json::value::Value;
7
8use crate::commit::{Commit, Range, commits_to_conventional_commits};
9use crate::config::{Bump, BumpType};
10use crate::error::Result;
11use crate::statistics::Statistics;
12#[cfg(feature = "remote")]
13use crate::{
14    contributor::RemoteContributor,
15    remote::{RemoteCommit, RemotePullRequest, RemoteReleaseMetadata},
16};
17
18/// Representation of a release.
19#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all(serialize = "camelCase"))]
21pub struct Release<'a> {
22    /// Release version, git tag.
23    pub version: Option<String>,
24    /// git tag's message.
25    pub message: Option<String>,
26    /// Commits made for the release.
27    #[serde(deserialize_with = "commits_to_conventional_commits")]
28    pub commits: Vec<Commit<'a>>,
29    /// Commit ID of the tag.
30    #[serde(rename = "commit_id")]
31    pub commit_id: Option<String>,
32    /// Timestamp of the release in seconds, from epoch.
33    pub timestamp: Option<i64>,
34    /// Previous release.
35    pub previous: Option<Box<Release<'a>>>,
36    /// Repository path.
37    pub repository: Option<String>,
38    /// Commit range.
39    #[serde(rename = "commit_range")]
40    pub commit_range: Option<Range>,
41    /// Submodule commits.
42    ///
43    /// Maps submodule path to a list of commits.
44    #[serde(rename = "submodule_commits")]
45    pub submodule_commits: HashMap<String, Vec<Commit<'a>>>,
46    /// Aggregated statistics computed from the release's commits.
47    pub statistics: Option<Statistics>,
48    /// Arbitrary data to be used with the `--from-context` CLI option.
49    pub extra: Option<Value>,
50    /// The type of version bump that was applied.
51    #[serde(rename = "bump_type")]
52    pub bump_type: Option<BumpType>,
53    /// Contributors.
54    #[cfg(feature = "github")]
55    pub github: RemoteReleaseMetadata,
56    /// Contributors.
57    #[cfg(feature = "gitlab")]
58    pub gitlab: RemoteReleaseMetadata,
59    /// Contributors.
60    #[cfg(feature = "gitea")]
61    pub gitea: RemoteReleaseMetadata,
62    /// Contributors.
63    #[cfg(feature = "bitbucket")]
64    pub bitbucket: RemoteReleaseMetadata,
65    /// Contributors.
66    #[cfg(feature = "azure_devops")]
67    #[serde(rename = "azure_devops")]
68    pub azure_devops: RemoteReleaseMetadata,
69}
70
71#[cfg(feature = "github")]
72crate::update_release_metadata!(github, update_github_metadata);
73
74#[cfg(feature = "gitlab")]
75crate::update_release_metadata!(gitlab, update_gitlab_metadata);
76
77#[cfg(feature = "gitea")]
78crate::update_release_metadata!(gitea, update_gitea_metadata);
79
80#[cfg(feature = "bitbucket")]
81crate::update_release_metadata!(bitbucket, update_bitbucket_metadata);
82
83#[cfg(feature = "azure_devops")]
84crate::update_release_metadata!(azure_devops, update_azure_devops_metadata);
85
86impl Release<'_> {
87    /// Calculates the next version based on the commits.
88    ///
89    /// It uses the default bump version configuration to calculate the next
90    /// version.
91    #[cfg_attr(
92        feature = "tracing",
93        tracing::instrument(
94            skip_all,
95            fields(
96                version = self.version.as_deref().unwrap_or("unreleased"),
97                commits = self.commits.len()
98            )
99        )
100    )]
101    pub fn calculate_next_version(&self) -> Result<NextVersion> {
102        crate::set_progress_message!("Calculating the next version from commits");
103        self.calculate_next_version_with_config(&Bump::default())
104    }
105
106    /// Returns a new `Release` instance with aggregated statistics populated.
107    ///
108    /// This method computes various statistics from the release data and sets
109    /// the `statistics` field. It does not modify the original release but
110    /// returns a new instance with the computed statistics included.
111    #[must_use]
112    pub fn with_statistics(mut self) -> Self {
113        self.statistics = Some((&self).into());
114        self
115    }
116
117    /// Calculates the next version based on the commits.
118    ///
119    /// It uses the given bump version configuration to calculate the next
120    /// version.
121    pub(super) fn calculate_next_version_with_config(&self, config: &Bump) -> Result<NextVersion> {
122        self.calculate_next_version_from_commits(config, true)
123    }
124
125    /// Calculates the next version based on the commits.
126    ///
127    /// When `conventional_commits` is disabled, bump evaluation uses only the
128    /// commit subject (the first line), so commit body text cannot trigger an
129    /// unintended version bump through the custom increment regular
130    /// expressions (`custom_major_increment_regex`,
131    /// `custom_minor_increment_regex` and `no_increment_regex`).
132    #[cfg_attr(
133        feature = "tracing",
134        tracing::instrument(
135            skip_all,
136            fields(
137                version = self.version.as_deref().unwrap_or("unreleased"),
138                commits = self.commits.len()
139            )
140        )
141    )]
142    pub(super) fn calculate_next_version_from_commits(
143        &self,
144        config: &Bump,
145        conventional_commits: bool,
146    ) -> Result<NextVersion> {
147        crate::set_progress_message!(
148            "Calculating the next version from commits with custom bump rules"
149        );
150        match self
151            .previous
152            .as_ref()
153            .and_then(|release| release.version.clone())
154        {
155            Some(version) => {
156                let mut semver = Version::parse(&version);
157                let mut prefix = None;
158                if semver.is_err() && version.split('.').count() >= 2 {
159                    let mut found_numeric = false;
160                    for (i, c) in version.char_indices() {
161                        if c.is_numeric() && !found_numeric {
162                            found_numeric = true;
163                            let version_prefix = version[..i].to_string();
164                            let remaining = version[i..].to_string();
165                            let version = Version::parse(&remaining);
166                            if version.is_ok() {
167                                semver = version;
168                                prefix = Some(version_prefix);
169                                break;
170                            }
171                        } else if !c.is_numeric() && found_numeric {
172                            found_numeric = false;
173                        }
174                    }
175                }
176                let mut next_version = VersionUpdater::new()
177                    .with_features_always_increment_minor(
178                        config.features_always_bump_minor.unwrap_or(true),
179                    )
180                    .with_breaking_always_increment_major(
181                        config.breaking_always_bump_major.unwrap_or(true),
182                    );
183                if let Some(custom_major_increment_regex) = &config.custom_major_increment_regex {
184                    next_version = next_version
185                        .with_custom_major_increment_regex(custom_major_increment_regex)?;
186                }
187                if let Some(custom_minor_increment_regex) = &config.custom_minor_increment_regex {
188                    next_version = next_version
189                        .with_custom_minor_increment_regex(custom_minor_increment_regex)?;
190                }
191                if let Some(no_increment_regex) = &config.no_increment_regex {
192                    next_version = next_version.with_no_increment_regex(no_increment_regex)?;
193                }
194                let old_semver = semver?;
195                let (next_version, determined_bump_type) =
196                    if let Some(bump_type) = &config.bump_type {
197                        let v = match bump_type {
198                            BumpType::Major => old_semver.increment_major().to_string(),
199                            BumpType::Minor => old_semver.increment_minor().to_string(),
200                            BumpType::Patch => old_semver.increment_patch().to_string(),
201                        };
202                        (v, Some(*bump_type))
203                    } else {
204                        let new_semver = next_version.increment(
205                            &old_semver,
206                            self.commits
207                                .iter()
208                                .map(|commit| {
209                                    let message = commit.raw_message().trim_end();
210                                    // When conventional commits are disabled,
211                                    // evaluate the bump regular expressions
212                                    // against the commit subject only, so body
213                                    // text cannot trigger an unintended version
214                                    // bump.
215                                    if conventional_commits {
216                                        message.to_string()
217                                    } else {
218                                        message.lines().next().unwrap_or_default().to_string()
219                                    }
220                                })
221                                .collect::<Vec<String>>(),
222                        );
223                        let bump_type = determine_bump_type(&old_semver, &new_semver);
224                        (new_semver.to_string(), bump_type)
225                    };
226                let version = if let Some(prefix) = prefix {
227                    format!("{prefix}{next_version}")
228                } else {
229                    next_version
230                };
231                Ok(NextVersion {
232                    version,
233                    bump_type: determined_bump_type,
234                })
235            }
236            None => Ok(NextVersion {
237                version: config.get_initial_tag(),
238                bump_type: None,
239            }),
240        }
241    }
242}
243
244/// Representation of a calculated next version.
245#[derive(Debug, Clone, PartialEq)]
246pub struct NextVersion {
247    /// Version string.
248    pub version: String,
249    /// Type of version bump that was applied.
250    pub bump_type: Option<BumpType>,
251}
252
253/// Determines the bump type by comparing two semver versions.
254fn determine_bump_type(old: &Version, new: &Version) -> Option<BumpType> {
255    if new.major != old.major {
256        Some(BumpType::Major)
257    } else if new.minor != old.minor {
258        Some(BumpType::Minor)
259    } else if new.patch != old.patch {
260        Some(BumpType::Patch)
261    } else {
262        None
263    }
264}
265
266/// Representation of a list of releases.
267#[derive(Serialize)]
268pub struct Releases<'a> {
269    /// Releases.
270    pub releases: &'a Vec<Release<'a>>,
271}
272
273impl Releases<'_> {
274    /// Returns the list of releases as JSON.
275    pub fn as_json(&self) -> Result<String> {
276        Ok(serde_json::to_string(self.releases)?)
277    }
278}
279
280#[cfg(test)]
281mod test {
282    use pretty_assertions::assert_eq;
283
284    use super::*;
285    #[test]
286    fn bump_version() -> Result<()> {
287        fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
288            Release {
289                version: None,
290                message: None,
291                extra: None,
292                commits: commits
293                    .iter()
294                    .map(|v| Commit::from((*v).to_string()))
295                    .collect(),
296                commit_range: None,
297                commit_id: None,
298                timestamp: None,
299                previous: Some(Box::new(Release {
300                    version: Some(String::from(version)),
301                    ..Default::default()
302                })),
303                repository: Some(String::from("/root/repo")),
304                submodule_commits: HashMap::new(),
305                statistics: None,
306                bump_type: None,
307                #[cfg(feature = "github")]
308                github: crate::remote::RemoteReleaseMetadata {
309                    contributors: vec![],
310                },
311                #[cfg(feature = "gitlab")]
312                gitlab: crate::remote::RemoteReleaseMetadata {
313                    contributors: vec![],
314                },
315                #[cfg(feature = "gitea")]
316                gitea: crate::remote::RemoteReleaseMetadata {
317                    contributors: vec![],
318                },
319                #[cfg(feature = "bitbucket")]
320                bitbucket: crate::remote::RemoteReleaseMetadata {
321                    contributors: vec![],
322                },
323                #[cfg(feature = "azure_devops")]
324                azure_devops: crate::remote::RemoteReleaseMetadata {
325                    contributors: vec![],
326                },
327            }
328        }
329
330        let test_shared = [
331            ("1.0.0", "1.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
332            ("1.0.0", "1.0.1", vec!["fix: add xyz", "fix: aaaaaa"]),
333            ("1.0.0", "2.0.0", vec!["feat!: add xyz", "feat: zzz"]),
334            ("1.0.0", "2.0.0", vec!["feat!: add xyz\n", "feat: zzz\n"]),
335            ("2.0.0", "2.0.1", vec!["fix: something"]),
336            ("foo/1.0.0", "foo/1.1.0", vec![
337                "feat: add xyz",
338                "fix: fix xyz",
339            ]),
340            ("bar/1.0.0", "bar/2.0.0", vec![
341                "fix: add xyz",
342                "fix!: aaaaaa",
343            ]),
344            ("zzz-123/test/1.0.0", "zzz-123/test/1.0.1", vec![
345                "fix: aaaaaa",
346            ]),
347            ("v100.0.0", "v101.0.0", vec!["feat!: something"]),
348            ("v1.0.0-alpha.1", "v1.0.0-alpha.2", vec!["fix: minor"]),
349            ("testing/v1.0.0-beta.1", "testing/v1.0.0-beta.2", vec![
350                "feat: nice",
351            ]),
352            ("tauri-v1.5.4", "tauri-v1.6.0", vec!["feat: something"]),
353            (
354                "rocket/rocket-v4.0.0-rc.1",
355                "rocket/rocket-v4.0.0-rc.2",
356                vec!["chore!: wow"],
357            ),
358            (
359                "aaa#/@#$@9384!#%^#@#@!#!239432413-idk-9999.2200.5932-alpha.419",
360                "aaa#/@#$@9384!#%^#@#@!#!239432413-idk-9999.2200.5932-alpha.420",
361                vec!["feat: damn this is working"],
362            ),
363        ];
364
365        for (version, expected_version, commits) in test_shared.iter().chain(
366            [
367                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
368                ("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
369                ("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
370                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
371                ("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]),
372                ("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
373            ]
374            .iter(),
375        ) {
376            let release = build_release(version, commits);
377            let next_version = release.calculate_next_version()?.version;
378            assert_eq!(expected_version, &next_version);
379            let next_version = release
380                .calculate_next_version_with_config(&Bump::default())?
381                .version;
382            assert_eq!(expected_version, &next_version);
383        }
384
385        for (version, expected_version, commits) in test_shared.iter().chain(
386            [
387                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
388                ("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]),
389                ("0.0.1", "0.0.2", vec!["feat!: add xyz", "feat: zzz"]),
390                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
391                ("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]),
392                ("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]),
393            ]
394            .iter(),
395        ) {
396            let release = build_release(version, commits);
397            let next_version = release
398                .calculate_next_version_with_config(&Bump {
399                    features_always_bump_minor: Some(false),
400                    breaking_always_bump_major: Some(false),
401                    initial_tag: None,
402                    custom_major_increment_regex: None,
403                    custom_minor_increment_regex: None,
404                    no_increment_regex: None,
405                    bump_type: None,
406                })?
407                .version;
408            assert_eq!(expected_version, &next_version);
409        }
410
411        for (version, expected_version, commits) in test_shared.iter().chain(
412            [
413                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
414                ("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
415                ("0.0.1", "0.1.0", vec!["feat!: add xyz", "feat: zzz"]),
416                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
417                ("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]),
418                ("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]),
419            ]
420            .iter(),
421        ) {
422            let release = build_release(version, commits);
423            let next_version = release
424                .calculate_next_version_with_config(&Bump {
425                    features_always_bump_minor: Some(true),
426                    breaking_always_bump_major: Some(false),
427                    initial_tag: None,
428                    custom_major_increment_regex: None,
429                    custom_minor_increment_regex: None,
430                    no_increment_regex: None,
431                    bump_type: None,
432                })?
433                .version;
434            assert_eq!(expected_version, &next_version);
435        }
436
437        for (version, expected_version, commits) in test_shared.iter().chain(
438            [
439                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
440                ("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]),
441                ("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
442                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
443                ("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]),
444                ("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
445            ]
446            .iter(),
447        ) {
448            let release = build_release(version, commits);
449            let next_version = release
450                .calculate_next_version_with_config(&Bump {
451                    features_always_bump_minor: Some(false),
452                    breaking_always_bump_major: Some(true),
453                    initial_tag: None,
454                    custom_major_increment_regex: None,
455                    custom_minor_increment_regex: None,
456                    no_increment_regex: None,
457                    bump_type: None,
458                })?
459                .version;
460            assert_eq!(expected_version, &next_version);
461        }
462
463        let empty_release = Release {
464            previous: Some(Box::new(Release {
465                version: None,
466                ..Default::default()
467            })),
468            ..Default::default()
469        };
470        let result = empty_release.calculate_next_version()?;
471        assert_eq!("0.1.0", result.version);
472        assert_eq!(None, result.bump_type);
473        for (features_always_bump_minor, breaking_always_bump_major) in
474            [(true, true), (true, false), (false, true), (false, false)]
475        {
476            let result = empty_release.calculate_next_version_with_config(&Bump {
477                features_always_bump_minor: Some(features_always_bump_minor),
478                breaking_always_bump_major: Some(breaking_always_bump_major),
479                initial_tag: None,
480                custom_major_increment_regex: None,
481                custom_minor_increment_regex: None,
482                no_increment_regex: None,
483                bump_type: None,
484            })?;
485            assert_eq!("0.1.0", result.version);
486            assert_eq!(None, result.bump_type);
487        }
488        Ok(())
489    }
490
491    #[test]
492    fn no_increment_regex_skips_matching_commit_types() -> Result<()> {
493        fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
494            Release {
495                version: None,
496                commits: commits
497                    .iter()
498                    .map(|v| Commit::from((*v).to_string()))
499                    .collect(),
500                previous: Some(Box::new(Release {
501                    version: Some(String::from(version)),
502                    ..Default::default()
503                })),
504                ..Default::default()
505            }
506        }
507
508        let release = build_release("1.0.0", &["chore: should not release"]);
509        let result = release.calculate_next_version_with_config(&Bump {
510            no_increment_regex: Some(String::from("^chore$")),
511            ..Default::default()
512        })?;
513        assert_eq!("1.0.0", result.version);
514        assert_eq!(None, result.bump_type);
515
516        let release = build_release("1.0.0", &[
517            "docs: update readme",
518            "feat: add a user-facing feature",
519        ]);
520        let result = release.calculate_next_version_with_config(&Bump {
521            no_increment_regex: Some(String::from("^docs$")),
522            ..Default::default()
523        })?;
524        assert_eq!("1.1.0", result.version);
525        assert_eq!(Some(BumpType::Minor), result.bump_type);
526
527        let release = Release {
528            version: None,
529            commits: vec![
530                Commit {
531                    message: String::from("test"),
532                    raw_message: Some(String::from("ci: test")),
533                    ..Default::default()
534                }
535                .into_conventional()?,
536            ],
537            previous: Some(Box::new(Release {
538                version: Some(String::from("1.0.0")),
539                ..Default::default()
540            })),
541            ..Default::default()
542        };
543        let result = release.calculate_next_version_with_config(&Bump {
544            no_increment_regex: Some(String::from("^ci$")),
545            ..Default::default()
546        })?;
547        assert_eq!("1.0.0", result.version);
548        assert_eq!(None, result.bump_type);
549
550        Ok(())
551    }
552
553    #[test]
554    fn custom_increment_regex_matches_subject_only_when_non_conventional() -> Result<()> {
555        fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
556            Release {
557                version: None,
558                commits: commits
559                    .iter()
560                    .map(|v| Commit::from((*v).to_string()))
561                    .collect(),
562                previous: Some(Box::new(Release {
563                    version: Some(String::from(version)),
564                    ..Default::default()
565                })),
566                ..Default::default()
567            }
568        }
569
570        let bump = Bump {
571            custom_major_increment_regex: Some(String::from("breaking")),
572            custom_minor_increment_regex: Some(String::from("feat")),
573            ..Default::default()
574        };
575
576        // The subject does not match any increment regex, but the body mentions
577        // both "breaking" and "feat". With conventional commits disabled, the
578        // body must be ignored so only a patch bump is produced.
579        // (https://github.com/orhun/git-cliff/issues/1476)
580        let release = build_release("1.0.0", &[
581            "fix a bug\n\nThe body mentions breaking and feat as plain words."
582        ]);
583        let result = release.calculate_next_version_from_commits(&bump, false)?;
584        assert_eq!("1.0.1", result.version);
585        assert_eq!(Some(BumpType::Patch), result.bump_type);
586
587        // Sanity: a matching subject still drives the expected bump.
588        let release = build_release("1.0.0", &["breaking: remove v1 endpoints\n\nBody text."]);
589        let result = release.calculate_next_version_from_commits(&bump, false)?;
590        assert_eq!("2.0.0", result.version);
591        assert_eq!(Some(BumpType::Major), result.bump_type);
592
593        let release = build_release("1.0.0", &["feat: add a thing\n\nBody text."]);
594        let result = release.calculate_next_version_from_commits(&bump, false)?;
595        assert_eq!("1.1.0", result.version);
596        assert_eq!(Some(BumpType::Minor), result.bump_type);
597
598        Ok(())
599    }
600
601    #[test]
602    fn no_increment_regex_uses_subject_only_when_non_conventional() -> Result<()> {
603        fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
604            Release {
605                version: None,
606                commits: commits
607                    .iter()
608                    .map(|v| Commit::from((*v).to_string()))
609                    .collect(),
610                previous: Some(Box::new(Release {
611                    version: Some(String::from(version)),
612                    ..Default::default()
613                })),
614                ..Default::default()
615            }
616        }
617
618        // The subject does not match `no_increment_regex`, but the body does.
619        // With conventional commits disabled, the body must be ignored so the
620        // commit is not suppressed and still produces a patch bump.
621        // (https://github.com/orhun/git-cliff/issues/1476)
622        let release = build_release("1.0.0", &[
623            "fix a bug\n\nThis is a chore cleanup and must not skip the bump."
624        ]);
625        let result = release.calculate_next_version_from_commits(
626            &Bump {
627                no_increment_regex: Some(String::from("chore")),
628                ..Default::default()
629            },
630            false,
631        )?;
632        assert_eq!("1.0.1", result.version);
633        assert_eq!(Some(BumpType::Patch), result.bump_type);
634
635        // Sanity: a matching subject still suppresses the bump.
636        let release = build_release("1.0.0", &["chore cleanup\n\nBody text."]);
637        let result = release.calculate_next_version_from_commits(
638            &Bump {
639                no_increment_regex: Some(String::from("chore")),
640                ..Default::default()
641            },
642            false,
643        )?;
644        assert_eq!("1.0.0", result.version);
645        assert_eq!(None, result.bump_type);
646
647        Ok(())
648    }
649
650    #[test]
651    fn bump_version_type() -> Result<()> {
652        fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
653            Release {
654                version: None,
655                commits: commits
656                    .iter()
657                    .map(|v| Commit::from((*v).to_string()))
658                    .collect(),
659                previous: Some(Box::new(Release {
660                    version: Some(String::from(version)),
661                    ..Default::default()
662                })),
663                ..Default::default()
664            }
665        }
666
667        let release = build_release("1.0.0", &["fix: something"]);
668        let result = release.calculate_next_version()?;
669        assert_eq!(Some(BumpType::Patch), result.bump_type);
670
671        let release = build_release("1.0.0", &["feat: add xyz"]);
672        let result = release.calculate_next_version()?;
673        assert_eq!(Some(BumpType::Minor), result.bump_type);
674
675        let release = build_release("1.0.0", &["feat!: breaking change"]);
676        let result = release.calculate_next_version()?;
677        assert_eq!(Some(BumpType::Major), result.bump_type);
678
679        let release = build_release("1.0.0", &["fix: something"]);
680        let result = release.calculate_next_version_with_config(&Bump {
681            bump_type: Some(BumpType::Minor),
682            ..Default::default()
683        })?;
684        assert_eq!("1.1.0", result.version);
685        assert_eq!(Some(BumpType::Minor), result.bump_type);
686
687        let release = Release {
688            previous: Some(Box::new(Release {
689                version: None,
690                ..Default::default()
691            })),
692            ..Default::default()
693        };
694        let result = release.calculate_next_version()?;
695        assert_eq!(None, result.bump_type);
696
697        Ok(())
698    }
699
700    #[test]
701    fn with_statistics() {
702        let release = Release {
703            commits: vec![],
704            timestamp: Some(1_649_373_910),
705            previous: Some(Box::new(Release {
706                timestamp: Some(1_649_201_110),
707                ..Default::default()
708            })),
709            repository: Some(String::from("/root/repo")),
710            ..Default::default()
711        };
712
713        assert!(release.statistics.is_none());
714        let release = release.with_statistics();
715        assert!(release.statistics.is_some());
716    }
717
718    #[cfg(feature = "github")]
719    #[test]
720    fn update_github_metadata() -> Result<()> {
721        use crate::remote::github::{
722            GitHubCommit, GitHubCommitAuthor, GitHubCommitDetails, GitHubCommitDetailsAuthor,
723            GitHubPullRequest, PullRequestLabel,
724        };
725
726        let mut release = Release {
727            version: None,
728            message: None,
729            extra: None,
730            commits: vec![
731                Commit::from(String::from(
732                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github integration",
733                )),
734                Commit::from(String::from(
735                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix github integration",
736                )),
737                Commit::from(String::from(
738                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
739                )),
740                Commit::from(String::from(
741                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
742                )),
743                Commit::from(String::from(
744                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
745                )),
746                Commit::from(String::from(
747                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
748                )),
749            ],
750            commit_range: None,
751            commit_id: None,
752            timestamp: None,
753            previous: Some(Box::new(Release {
754                version: Some(String::from("1.0.0")),
755                ..Default::default()
756            })),
757            repository: Some(String::from("/root/repo")),
758            submodule_commits: HashMap::new(),
759            statistics: None,
760            bump_type: None,
761            github: RemoteReleaseMetadata {
762                contributors: vec![],
763            },
764            #[cfg(feature = "gitlab")]
765            gitlab: RemoteReleaseMetadata {
766                contributors: vec![],
767            },
768            #[cfg(feature = "gitea")]
769            gitea: RemoteReleaseMetadata {
770                contributors: vec![],
771            },
772            #[cfg(feature = "bitbucket")]
773            bitbucket: RemoteReleaseMetadata {
774                contributors: vec![],
775            },
776            #[cfg(feature = "azure_devops")]
777            azure_devops: RemoteReleaseMetadata {
778                contributors: vec![],
779            },
780        };
781        release.update_github_metadata(
782            vec![
783                GitHubCommit {
784                    sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
785                    author: Some(GitHubCommitAuthor {
786                        login: Some(String::from("orhun")),
787                    }),
788                    commit: Some(GitHubCommitDetails {
789                        author: GitHubCommitDetailsAuthor {
790                            date: String::from("2021-07-18T15:14:39+03:00"),
791                        },
792                    }),
793                },
794                GitHubCommit {
795                    sha: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
796                    author: Some(GitHubCommitAuthor {
797                        login: Some(String::from("orhun")),
798                    }),
799                    commit: Some(GitHubCommitDetails {
800                        author: GitHubCommitDetailsAuthor {
801                            date: String::from("2021-07-18T15:12:19+03:00"),
802                        },
803                    }),
804                },
805                GitHubCommit {
806                    sha: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
807                    author: Some(GitHubCommitAuthor {
808                        login: Some(String::from("nuhro")),
809                    }),
810                    commit: Some(GitHubCommitDetails {
811                        author: GitHubCommitDetailsAuthor {
812                            date: String::from("2021-07-18T15:07:23+03:00"),
813                        },
814                    }),
815                },
816                GitHubCommit {
817                    sha: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
818                    author: Some(GitHubCommitAuthor {
819                        login: Some(String::from("awesome_contributor")),
820                    }),
821                    commit: Some(GitHubCommitDetails {
822                        author: GitHubCommitDetailsAuthor {
823                            date: String::from("2021-07-18T15:05:10+03:00"),
824                        },
825                    }),
826                },
827                GitHubCommit {
828                    sha: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
829                    author: Some(GitHubCommitAuthor {
830                        login: Some(String::from("orhun")),
831                    }),
832                    commit: Some(GitHubCommitDetails {
833                        author: GitHubCommitDetailsAuthor {
834                            date: String::from("2021-07-18T15:03:30+03:00"),
835                        },
836                    }),
837                },
838                GitHubCommit {
839                    sha: String::from("6c34967147560ea09658776d4901709139b4ad66"),
840                    author: Some(GitHubCommitAuthor {
841                        login: Some(String::from("someone")),
842                    }),
843                    commit: Some(GitHubCommitDetails {
844                        author: GitHubCommitDetailsAuthor {
845                            date: String::from("2021-07-18T15:00:38+03:00"),
846                        },
847                    }),
848                },
849                GitHubCommit {
850                    sha: String::from("0c34967147560e809658776d4901709139b4ad68"),
851                    author: Some(GitHubCommitAuthor {
852                        login: Some(String::from("idk")),
853                    }),
854                    commit: Some(GitHubCommitDetails {
855                        author: GitHubCommitDetailsAuthor {
856                            date: String::from("2021-07-18T15:00:38+03:00"),
857                        },
858                    }),
859                },
860                GitHubCommit {
861                    sha: String::from("kk34967147560e809658776d4901709139b4ad68"),
862                    author: None,
863                    commit: None,
864                },
865                GitHubCommit {
866                    sha: String::new(),
867                    author: None,
868                    commit: None,
869                },
870            ]
871            .into_iter()
872            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
873            .collect(),
874            vec![
875                GitHubPullRequest {
876                    user: Some(GitHubCommitAuthor {
877                        login: Some(String::from("author-1")),
878                    }),
879                    title: Some(String::from("1")),
880                    number: 42,
881                    merge_commit_sha: Some(String::from(
882                        "1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
883                    )),
884                    labels: vec![PullRequestLabel {
885                        name: String::from("rust"),
886                    }],
887                },
888                GitHubPullRequest {
889                    user: Some(GitHubCommitAuthor {
890                        login: Some(String::from("author-2")),
891                    }),
892                    title: Some(String::from("2")),
893                    number: 66,
894                    merge_commit_sha: Some(String::from(
895                        "21f6aa587fcb772de13f2fde0e92697c51f84162",
896                    )),
897                    labels: vec![PullRequestLabel {
898                        name: String::from("rust"),
899                    }],
900                },
901                GitHubPullRequest {
902                    user: Some(GitHubCommitAuthor {
903                        login: Some(String::from("author-3")),
904                    }),
905                    title: Some(String::from("3")),
906                    number: 53,
907                    merge_commit_sha: Some(String::from(
908                        "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
909                    )),
910                    labels: vec![PullRequestLabel {
911                        name: String::from("deps"),
912                    }],
913                },
914                GitHubPullRequest {
915                    user: Some(GitHubCommitAuthor {
916                        login: Some(String::from("author-4")),
917                    }),
918                    title: Some(String::from("4")),
919                    number: 1_000,
920                    merge_commit_sha: Some(String::from(
921                        "4d3ffe4753b923f4d7807c490e650e6624a12074",
922                    )),
923                    labels: vec![PullRequestLabel {
924                        name: String::from("deps"),
925                    }],
926                },
927                GitHubPullRequest {
928                    user: Some(GitHubCommitAuthor {
929                        login: Some(String::from("author-5")),
930                    }),
931                    title: Some(String::from("5")),
932                    number: 999_999,
933                    merge_commit_sha: Some(String::from(
934                        "5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
935                    )),
936                    labels: vec![PullRequestLabel {
937                        name: String::from("github"),
938                    }],
939                },
940            ]
941            .into_iter()
942            .map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
943            .collect(),
944        )?;
945        #[allow(deprecated)]
946        let expected_commits = vec![
947            Commit {
948                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
949                message: String::from("add github integration"),
950                github: RemoteContributor {
951                    username: Some(String::from("orhun")),
952                    pr_author: Some(String::from("author-1")),
953                    pr_title: Some(String::from("1")),
954                    pr_number: Some(42),
955                    pr_numbers: vec![],
956                    pr_labels: vec![String::from("rust")],
957                    is_first_time: false,
958                },
959                remote: Some(RemoteContributor {
960                    username: Some(String::from("orhun")),
961                    pr_author: Some(String::from("author-1")),
962                    pr_title: Some(String::from("1")),
963                    pr_number: Some(42),
964                    pr_numbers: vec![],
965                    pr_labels: vec![String::from("rust")],
966                    is_first_time: false,
967                }),
968                ..Default::default()
969            },
970            Commit {
971                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
972                message: String::from("fix github integration"),
973                github: RemoteContributor {
974                    username: Some(String::from("orhun")),
975                    pr_author: Some(String::from("author-2")),
976                    pr_title: Some(String::from("2")),
977                    pr_number: Some(66),
978                    pr_numbers: vec![],
979                    pr_labels: vec![String::from("rust")],
980                    is_first_time: false,
981                },
982                remote: Some(RemoteContributor {
983                    username: Some(String::from("orhun")),
984                    pr_author: Some(String::from("author-2")),
985                    pr_title: Some(String::from("2")),
986                    pr_number: Some(66),
987                    pr_numbers: vec![],
988                    pr_labels: vec![String::from("rust")],
989                    is_first_time: false,
990                }),
991                ..Default::default()
992            },
993            Commit {
994                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
995                message: String::from("update metadata"),
996                github: RemoteContributor {
997                    username: Some(String::from("nuhro")),
998                    pr_author: Some(String::from("author-3")),
999                    pr_title: Some(String::from("3")),
1000                    pr_number: Some(53),
1001                    pr_numbers: vec![],
1002                    pr_labels: vec![String::from("deps")],
1003                    is_first_time: false,
1004                },
1005                remote: Some(RemoteContributor {
1006                    username: Some(String::from("nuhro")),
1007                    pr_author: Some(String::from("author-3")),
1008                    pr_title: Some(String::from("3")),
1009                    pr_number: Some(53),
1010                    pr_numbers: vec![],
1011                    pr_labels: vec![String::from("deps")],
1012                    is_first_time: false,
1013                }),
1014                ..Default::default()
1015            },
1016            Commit {
1017                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1018                message: String::from("do some stuff"),
1019                github: RemoteContributor {
1020                    username: Some(String::from("awesome_contributor")),
1021                    pr_author: Some(String::from("author-4")),
1022                    pr_title: Some(String::from("4")),
1023                    pr_number: Some(1_000),
1024                    pr_numbers: vec![],
1025                    pr_labels: vec![String::from("deps")],
1026                    is_first_time: false,
1027                },
1028                remote: Some(RemoteContributor {
1029                    username: Some(String::from("awesome_contributor")),
1030                    pr_author: Some(String::from("author-4")),
1031                    pr_title: Some(String::from("4")),
1032                    pr_number: Some(1_000),
1033                    pr_numbers: vec![],
1034                    pr_labels: vec![String::from("deps")],
1035                    is_first_time: false,
1036                }),
1037                ..Default::default()
1038            },
1039            Commit {
1040                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1041                message: String::from("alright"),
1042                github: RemoteContributor {
1043                    username: Some(String::from("orhun")),
1044                    pr_author: Some(String::from("author-5")),
1045                    pr_title: Some(String::from("5")),
1046                    pr_number: Some(999_999),
1047                    pr_numbers: vec![],
1048                    pr_labels: vec![String::from("github")],
1049                    is_first_time: false,
1050                },
1051                remote: Some(RemoteContributor {
1052                    username: Some(String::from("orhun")),
1053                    pr_author: Some(String::from("author-5")),
1054                    pr_title: Some(String::from("5")),
1055                    pr_number: Some(999_999),
1056                    pr_numbers: vec![],
1057                    pr_labels: vec![String::from("github")],
1058                    is_first_time: false,
1059                }),
1060                ..Default::default()
1061            },
1062            Commit {
1063                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1064                message: String::from("should be fine"),
1065                github: RemoteContributor {
1066                    username: Some(String::from("someone")),
1067                    pr_author: None,
1068                    pr_title: None,
1069                    pr_number: None,
1070                    pr_numbers: vec![],
1071                    pr_labels: vec![],
1072                    is_first_time: false,
1073                },
1074                remote: Some(RemoteContributor {
1075                    username: Some(String::from("someone")),
1076                    pr_author: None,
1077                    pr_title: None,
1078                    pr_number: None,
1079                    pr_numbers: vec![],
1080                    pr_labels: vec![],
1081                    is_first_time: false,
1082                }),
1083                ..Default::default()
1084            },
1085        ];
1086        assert_eq!(expected_commits, release.commits);
1087
1088        release.github.contributors.sort_by_key(|a| a.pr_number);
1089
1090        let expected_metadata = RemoteReleaseMetadata {
1091            contributors: vec![
1092                RemoteContributor {
1093                    username: Some(String::from("someone")),
1094                    pr_author: None,
1095                    pr_title: None,
1096                    pr_number: None,
1097                    pr_numbers: vec![],
1098                    pr_labels: vec![],
1099                    is_first_time: true,
1100                },
1101                RemoteContributor {
1102                    username: Some(String::from("orhun")),
1103                    pr_author: None,
1104                    pr_title: Some(String::from("1")),
1105                    pr_number: Some(42),
1106                    pr_numbers: vec![42, 66, 999_999],
1107                    pr_labels: vec![String::from("rust")],
1108                    is_first_time: true,
1109                },
1110                RemoteContributor {
1111                    username: Some(String::from("nuhro")),
1112                    pr_author: None,
1113                    pr_title: Some(String::from("3")),
1114                    pr_number: Some(53),
1115                    pr_numbers: vec![53],
1116                    pr_labels: vec![String::from("deps")],
1117                    is_first_time: true,
1118                },
1119                RemoteContributor {
1120                    username: Some(String::from("awesome_contributor")),
1121                    pr_author: None,
1122                    pr_title: Some(String::from("4")),
1123                    pr_number: Some(1_000),
1124                    pr_numbers: vec![1_000],
1125                    pr_labels: vec![String::from("deps")],
1126                    is_first_time: true,
1127                },
1128            ],
1129        };
1130        assert_eq!(expected_metadata, release.github);
1131
1132        Ok(())
1133    }
1134
1135    #[cfg(feature = "gitlab")]
1136    #[test]
1137    fn update_gitlab_metadata() -> Result<()> {
1138        use crate::remote::gitlab::{GitLabCommit, GitLabMergeRequest, GitLabUser};
1139
1140        let mut release = Release {
1141            version: None,
1142            message: None,
1143            extra: None,
1144            commits: vec![
1145                Commit::from(String::from(
1146                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github integration",
1147                )),
1148                Commit::from(String::from(
1149                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix github integration",
1150                )),
1151                Commit::from(String::from(
1152                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
1153                )),
1154                Commit::from(String::from(
1155                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
1156                )),
1157                Commit::from(String::from(
1158                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
1159                )),
1160                Commit::from(String::from(
1161                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
1162                )),
1163            ],
1164            commit_range: None,
1165            commit_id: None,
1166            timestamp: None,
1167            previous: Some(Box::new(Release {
1168                version: Some(String::from("1.0.0")),
1169                ..Default::default()
1170            })),
1171            repository: Some(String::from("/root/repo")),
1172            submodule_commits: HashMap::new(),
1173            statistics: None,
1174            bump_type: None,
1175            #[cfg(feature = "github")]
1176            github: RemoteReleaseMetadata {
1177                contributors: vec![],
1178            },
1179            #[cfg(feature = "gitlab")]
1180            gitlab: RemoteReleaseMetadata {
1181                contributors: vec![],
1182            },
1183            #[cfg(feature = "gitea")]
1184            gitea: RemoteReleaseMetadata {
1185                contributors: vec![],
1186            },
1187            #[cfg(feature = "bitbucket")]
1188            bitbucket: RemoteReleaseMetadata {
1189                contributors: vec![],
1190            },
1191            #[cfg(feature = "azure_devops")]
1192            azure_devops: RemoteReleaseMetadata {
1193                contributors: vec![],
1194            },
1195        };
1196        release.update_gitlab_metadata(
1197            vec![
1198                GitLabCommit {
1199                    id: Some(String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071")),
1200                    author_name: Some(String::from("orhun")),
1201                    short_id: Some(String::new()),
1202                    title: Some(String::new()),
1203                    author_email: Some(String::new()),
1204                    authored_date: Some(String::new()),
1205                    committer_name: Some(String::new()),
1206                    committer_email: Some(String::new()),
1207                    committed_date: Some(String::new()),
1208                    created_at: Some(String::new()),
1209                    message: Some(String::new()),
1210                    parent_ids: vec![],
1211                    web_url: Some(String::new()),
1212                },
1213                GitLabCommit {
1214                    id: Some(String::from("21f6aa587fcb772de13f2fde0e92697c51f84162")),
1215                    author_name: Some(String::from("orhun")),
1216                    short_id: Some(String::new()),
1217                    title: Some(String::new()),
1218                    author_email: Some(String::new()),
1219                    authored_date: Some(String::new()),
1220                    committer_name: Some(String::new()),
1221                    committer_email: Some(String::new()),
1222                    committed_date: Some(String::new()),
1223                    created_at: Some(String::new()),
1224                    message: Some(String::new()),
1225                    parent_ids: vec![],
1226                    web_url: Some(String::new()),
1227                },
1228                GitLabCommit {
1229                    id: Some(String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973")),
1230                    author_name: Some(String::from("nuhro")),
1231                    short_id: Some(String::new()),
1232                    title: Some(String::new()),
1233                    author_email: Some(String::new()),
1234                    authored_date: Some(String::new()),
1235                    committer_name: Some(String::new()),
1236                    committer_email: Some(String::new()),
1237                    committed_date: Some(String::new()),
1238                    created_at: Some(String::new()),
1239                    message: Some(String::new()),
1240                    parent_ids: vec![],
1241                    web_url: Some(String::new()),
1242                },
1243                GitLabCommit {
1244                    id: Some(String::from("4d3ffe4753b923f4d7807c490e650e6624a12074")),
1245                    author_name: Some(String::from("awesome_contributor")),
1246                    short_id: Some(String::new()),
1247                    title: Some(String::new()),
1248                    author_email: Some(String::new()),
1249                    authored_date: Some(String::new()),
1250                    committer_name: Some(String::new()),
1251                    committer_email: Some(String::new()),
1252                    committed_date: Some(String::new()),
1253                    created_at: Some(String::new()),
1254                    message: Some(String::new()),
1255                    parent_ids: vec![],
1256                    web_url: Some(String::new()),
1257                },
1258                GitLabCommit {
1259                    id: Some(String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05")),
1260                    author_name: Some(String::from("orhun")),
1261                    short_id: Some(String::new()),
1262                    title: Some(String::new()),
1263                    author_email: Some(String::new()),
1264                    authored_date: Some(String::new()),
1265                    committer_name: Some(String::new()),
1266                    committer_email: Some(String::new()),
1267                    committed_date: Some(String::new()),
1268                    created_at: Some(String::new()),
1269                    message: Some(String::new()),
1270                    parent_ids: vec![],
1271                    web_url: Some(String::new()),
1272                },
1273                GitLabCommit {
1274                    id: Some(String::from("6c34967147560ea09658776d4901709139b4ad66")),
1275                    author_name: Some(String::from("someone")),
1276                    short_id: Some(String::new()),
1277                    title: Some(String::new()),
1278                    author_email: Some(String::new()),
1279                    authored_date: Some(String::new()),
1280                    committer_name: Some(String::new()),
1281                    committer_email: Some(String::new()),
1282                    committed_date: Some(String::new()),
1283                    created_at: Some(String::new()),
1284                    message: Some(String::new()),
1285                    parent_ids: vec![],
1286                    web_url: Some(String::new()),
1287                },
1288                GitLabCommit {
1289                    id: Some(String::from("0c34967147560e809658776d4901709139b4ad68")),
1290                    author_name: Some(String::from("idk")),
1291                    short_id: Some(String::new()),
1292                    title: Some(String::new()),
1293                    author_email: Some(String::new()),
1294                    authored_date: Some(String::new()),
1295                    committer_name: Some(String::new()),
1296                    committer_email: Some(String::new()),
1297                    committed_date: Some(String::new()),
1298                    created_at: Some(String::new()),
1299                    message: Some(String::new()),
1300                    parent_ids: vec![],
1301                    web_url: Some(String::new()),
1302                },
1303                GitLabCommit {
1304                    id: Some(String::from("kk34967147560e809658776d4901709139b4ad68")),
1305                    author_name: Some(String::from("orhun")),
1306                    short_id: Some(String::new()),
1307                    title: Some(String::new()),
1308                    author_email: Some(String::new()),
1309                    authored_date: Some(String::new()),
1310                    committer_name: Some(String::new()),
1311                    committer_email: Some(String::new()),
1312                    committed_date: Some(String::new()),
1313                    created_at: Some(String::new()),
1314                    message: Some(String::new()),
1315                    parent_ids: vec![],
1316                    web_url: Some(String::new()),
1317                },
1318            ]
1319            .into_iter()
1320            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
1321            .collect(),
1322            vec![Box::new(GitLabMergeRequest {
1323                title: Some(String::from("1")),
1324                merge_commit_sha: Some(String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071")),
1325                id: Some(1),
1326                iid: Some(1),
1327                project_id: Some(1),
1328                description: Some(String::new()),
1329                state: Some(String::new()),
1330                created_at: Some(String::new()),
1331                author: Some(GitLabUser {
1332                    id: Some(1),
1333                    name: Some(String::from("42")),
1334                    username: Some(String::from("author-1")),
1335                    state: Some(String::from("42")),
1336                    avatar_url: None,
1337                    web_url: Some(String::from("42")),
1338                }),
1339                sha: Some(String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071")),
1340                web_url: Some(String::new()),
1341                squash_commit_sha: None,
1342                labels: vec![String::from("rust")],
1343            })],
1344        )?;
1345        #[allow(deprecated)]
1346        let expected_commits = vec![
1347            Commit {
1348                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1349                message: String::from("add github integration"),
1350                gitlab: RemoteContributor {
1351                    username: Some(String::from("orhun")),
1352                    pr_author: Some(String::from("author-1")),
1353                    pr_title: Some(String::from("1")),
1354                    pr_number: Some(1),
1355                    pr_numbers: vec![],
1356                    pr_labels: vec![String::from("rust")],
1357                    is_first_time: false,
1358                },
1359                remote: Some(RemoteContributor {
1360                    username: Some(String::from("orhun")),
1361                    pr_author: Some(String::from("author-1")),
1362                    pr_title: Some(String::from("1")),
1363                    pr_number: Some(1),
1364                    pr_numbers: vec![],
1365                    pr_labels: vec![String::from("rust")],
1366                    is_first_time: false,
1367                }),
1368                ..Default::default()
1369            },
1370            Commit {
1371                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1372                message: String::from("fix github integration"),
1373                gitlab: RemoteContributor {
1374                    username: Some(String::from("orhun")),
1375                    pr_author: None,
1376                    pr_title: None,
1377                    pr_number: None,
1378                    pr_numbers: vec![],
1379                    pr_labels: vec![],
1380                    is_first_time: false,
1381                },
1382                remote: Some(RemoteContributor {
1383                    username: Some(String::from("orhun")),
1384                    pr_author: None,
1385                    pr_title: None,
1386                    pr_number: None,
1387                    pr_numbers: vec![],
1388                    pr_labels: vec![],
1389                    is_first_time: false,
1390                }),
1391                ..Default::default()
1392            },
1393            Commit {
1394                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1395                message: String::from("update metadata"),
1396                gitlab: RemoteContributor {
1397                    username: Some(String::from("nuhro")),
1398                    pr_author: None,
1399                    pr_title: None,
1400                    pr_number: None,
1401                    pr_numbers: vec![],
1402                    pr_labels: vec![],
1403                    is_first_time: false,
1404                },
1405                remote: Some(RemoteContributor {
1406                    username: Some(String::from("nuhro")),
1407                    pr_author: None,
1408                    pr_title: None,
1409                    pr_number: None,
1410                    pr_numbers: vec![],
1411                    pr_labels: vec![],
1412                    is_first_time: false,
1413                }),
1414                ..Default::default()
1415            },
1416            Commit {
1417                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1418                message: String::from("do some stuff"),
1419                gitlab: RemoteContributor {
1420                    username: Some(String::from("awesome_contributor")),
1421                    pr_author: None,
1422                    pr_title: None,
1423                    pr_number: None,
1424                    pr_numbers: vec![],
1425                    pr_labels: vec![],
1426                    is_first_time: false,
1427                },
1428                remote: Some(RemoteContributor {
1429                    username: Some(String::from("awesome_contributor")),
1430                    pr_author: None,
1431                    pr_title: None,
1432                    pr_number: None,
1433                    pr_numbers: vec![],
1434                    pr_labels: vec![],
1435                    is_first_time: false,
1436                }),
1437                ..Default::default()
1438            },
1439            Commit {
1440                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1441                message: String::from("alright"),
1442                gitlab: RemoteContributor {
1443                    username: Some(String::from("orhun")),
1444                    pr_author: None,
1445                    pr_title: None,
1446                    pr_number: None,
1447                    pr_numbers: vec![],
1448                    pr_labels: vec![],
1449                    is_first_time: false,
1450                },
1451                remote: Some(RemoteContributor {
1452                    username: Some(String::from("orhun")),
1453                    pr_author: None,
1454                    pr_title: None,
1455                    pr_number: None,
1456                    pr_numbers: vec![],
1457                    pr_labels: vec![],
1458                    is_first_time: false,
1459                }),
1460                ..Default::default()
1461            },
1462            Commit {
1463                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1464                message: String::from("should be fine"),
1465                gitlab: RemoteContributor {
1466                    username: Some(String::from("someone")),
1467                    pr_author: None,
1468                    pr_title: None,
1469                    pr_number: None,
1470                    pr_numbers: vec![],
1471                    pr_labels: vec![],
1472                    is_first_time: false,
1473                },
1474                remote: Some(RemoteContributor {
1475                    username: Some(String::from("someone")),
1476                    pr_author: None,
1477                    pr_title: None,
1478                    pr_number: None,
1479                    pr_numbers: vec![],
1480                    pr_labels: vec![],
1481                    is_first_time: false,
1482                }),
1483                ..Default::default()
1484            },
1485        ];
1486        assert_eq!(expected_commits, release.commits);
1487
1488        release.github.contributors.sort_by_key(|a| a.pr_number);
1489
1490        let expected_metadata = RemoteReleaseMetadata {
1491            contributors: vec![
1492                RemoteContributor {
1493                    username: Some(String::from("orhun")),
1494                    pr_author: None,
1495                    pr_title: Some(String::from("1")),
1496                    pr_number: Some(1),
1497                    pr_numbers: vec![1],
1498                    pr_labels: vec![String::from("rust")],
1499                    is_first_time: false,
1500                },
1501                RemoteContributor {
1502                    username: Some(String::from("nuhro")),
1503                    pr_author: None,
1504                    pr_title: None,
1505                    pr_number: None,
1506                    pr_numbers: vec![],
1507                    pr_labels: vec![],
1508                    is_first_time: true,
1509                },
1510                RemoteContributor {
1511                    username: Some(String::from("awesome_contributor")),
1512                    pr_author: None,
1513                    pr_title: None,
1514                    pr_number: None,
1515                    pr_numbers: vec![],
1516                    pr_labels: vec![],
1517                    is_first_time: true,
1518                },
1519                RemoteContributor {
1520                    username: Some(String::from("someone")),
1521                    pr_author: None,
1522                    pr_title: None,
1523                    pr_number: None,
1524                    pr_numbers: vec![],
1525                    pr_labels: vec![],
1526                    is_first_time: true,
1527                },
1528            ],
1529        };
1530        assert_eq!(expected_metadata, release.gitlab);
1531
1532        Ok(())
1533    }
1534
1535    #[cfg(feature = "gitea")]
1536    #[test]
1537    fn update_gitea_metadata() -> Result<()> {
1538        use crate::remote::gitea::{
1539            GiteaCommit, GiteaCommitAuthor, GiteaPullRequest, PullRequestLabel,
1540        };
1541
1542        let mut release = Release {
1543            version: None,
1544            message: None,
1545            extra: None,
1546            commits: vec![
1547                Commit::from(String::from(
1548                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github integration",
1549                )),
1550                Commit::from(String::from(
1551                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix github integration",
1552                )),
1553                Commit::from(String::from(
1554                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
1555                )),
1556                Commit::from(String::from(
1557                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
1558                )),
1559                Commit::from(String::from(
1560                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
1561                )),
1562                Commit::from(String::from(
1563                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
1564                )),
1565            ],
1566            commit_range: None,
1567            commit_id: None,
1568            timestamp: None,
1569            previous: Some(Box::new(Release {
1570                version: Some(String::from("1.0.0")),
1571                ..Default::default()
1572            })),
1573            repository: Some(String::from("/root/repo")),
1574            submodule_commits: HashMap::new(),
1575            statistics: None,
1576            bump_type: None,
1577            #[cfg(feature = "github")]
1578            github: RemoteReleaseMetadata {
1579                contributors: vec![],
1580            },
1581            #[cfg(feature = "gitlab")]
1582            gitlab: RemoteReleaseMetadata {
1583                contributors: vec![],
1584            },
1585            #[cfg(feature = "gitea")]
1586            gitea: RemoteReleaseMetadata {
1587                contributors: vec![],
1588            },
1589            #[cfg(feature = "bitbucket")]
1590            bitbucket: RemoteReleaseMetadata {
1591                contributors: vec![],
1592            },
1593            #[cfg(feature = "azure_devops")]
1594            azure_devops: RemoteReleaseMetadata {
1595                contributors: vec![],
1596            },
1597        };
1598        release.update_gitea_metadata(
1599            vec![
1600                GiteaCommit {
1601                    sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1602                    author: Some(GiteaCommitAuthor {
1603                        login: Some(String::from("orhun")),
1604                    }),
1605                    created: String::from("2021-07-18T15:14:39+03:00"),
1606                },
1607                GiteaCommit {
1608                    sha: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1609                    author: Some(GiteaCommitAuthor {
1610                        login: Some(String::from("orhun")),
1611                    }),
1612                    created: String::from("2021-07-18T15:12:19+03:00"),
1613                },
1614                GiteaCommit {
1615                    sha: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1616                    author: Some(GiteaCommitAuthor {
1617                        login: Some(String::from("nuhro")),
1618                    }),
1619                    created: String::from("2021-07-18T15:07:23+03:00"),
1620                },
1621                GiteaCommit {
1622                    sha: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1623                    author: Some(GiteaCommitAuthor {
1624                        login: Some(String::from("awesome_contributor")),
1625                    }),
1626                    created: String::from("2021-07-18T15:05:10+03:00"),
1627                },
1628                GiteaCommit {
1629                    sha: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1630                    author: Some(GiteaCommitAuthor {
1631                        login: Some(String::from("orhun")),
1632                    }),
1633                    created: String::from("2021-07-18T15:03:30+03:00"),
1634                },
1635                GiteaCommit {
1636                    sha: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1637                    author: Some(GiteaCommitAuthor {
1638                        login: Some(String::from("someone")),
1639                    }),
1640                    created: String::from("2021-07-18T15:00:38+03:00"),
1641                },
1642                GiteaCommit {
1643                    sha: String::from("0c34967147560e809658776d4901709139b4ad68"),
1644                    author: Some(GiteaCommitAuthor {
1645                        login: Some(String::from("idk")),
1646                    }),
1647                    created: String::from("2021-07-18T15:00:38+03:00"),
1648                },
1649                GiteaCommit {
1650                    sha: String::from("kk34967147560e809658776d4901709139b4ad68"),
1651                    author: None,
1652                    created: String::new(),
1653                },
1654                GiteaCommit {
1655                    sha: String::new(),
1656                    author: None,
1657                    created: String::new(),
1658                },
1659            ]
1660            .into_iter()
1661            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
1662            .collect(),
1663            vec![
1664                GiteaPullRequest {
1665                    user: Some(GiteaCommitAuthor {
1666                        login: Some(String::from("author-1")),
1667                    }),
1668                    title: Some(String::from("1")),
1669                    number: 42,
1670                    merge_commit_sha: Some(String::from(
1671                        "1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
1672                    )),
1673                    labels: vec![PullRequestLabel {
1674                        name: String::from("rust"),
1675                    }],
1676                },
1677                GiteaPullRequest {
1678                    user: Some(GiteaCommitAuthor {
1679                        login: Some(String::from("author-2")),
1680                    }),
1681                    title: Some(String::from("2")),
1682                    number: 66,
1683                    merge_commit_sha: Some(String::from(
1684                        "21f6aa587fcb772de13f2fde0e92697c51f84162",
1685                    )),
1686                    labels: vec![PullRequestLabel {
1687                        name: String::from("rust"),
1688                    }],
1689                },
1690                GiteaPullRequest {
1691                    user: Some(GiteaCommitAuthor {
1692                        login: Some(String::from("author-3")),
1693                    }),
1694                    title: Some(String::from("3")),
1695                    number: 53,
1696                    merge_commit_sha: Some(String::from(
1697                        "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
1698                    )),
1699                    labels: vec![PullRequestLabel {
1700                        name: String::from("deps"),
1701                    }],
1702                },
1703                GiteaPullRequest {
1704                    user: Some(GiteaCommitAuthor {
1705                        login: Some(String::from("author-4")),
1706                    }),
1707                    title: Some(String::from("4")),
1708                    number: 1_000,
1709                    merge_commit_sha: Some(String::from(
1710                        "4d3ffe4753b923f4d7807c490e650e6624a12074",
1711                    )),
1712                    labels: vec![PullRequestLabel {
1713                        name: String::from("deps"),
1714                    }],
1715                },
1716                GiteaPullRequest {
1717                    user: Some(GiteaCommitAuthor {
1718                        login: Some(String::from("author-5")),
1719                    }),
1720                    title: Some(String::from("5")),
1721                    number: 999_999,
1722                    merge_commit_sha: Some(String::from(
1723                        "5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
1724                    )),
1725                    labels: vec![PullRequestLabel {
1726                        name: String::from("github"),
1727                    }],
1728                },
1729            ]
1730            .into_iter()
1731            .map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
1732            .collect(),
1733        )?;
1734        #[allow(deprecated)]
1735        let expected_commits = vec![
1736            Commit {
1737                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1738                message: String::from("add github integration"),
1739                gitea: RemoteContributor {
1740                    username: Some(String::from("orhun")),
1741                    pr_author: Some(String::from("author-1")),
1742                    pr_title: Some(String::from("1")),
1743                    pr_number: Some(42),
1744                    pr_numbers: vec![],
1745                    pr_labels: vec![String::from("rust")],
1746                    is_first_time: false,
1747                },
1748                remote: Some(RemoteContributor {
1749                    username: Some(String::from("orhun")),
1750                    pr_author: Some(String::from("author-1")),
1751                    pr_title: Some(String::from("1")),
1752                    pr_number: Some(42),
1753                    pr_numbers: vec![],
1754                    pr_labels: vec![String::from("rust")],
1755                    is_first_time: false,
1756                }),
1757                ..Default::default()
1758            },
1759            Commit {
1760                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1761                message: String::from("fix github integration"),
1762                gitea: RemoteContributor {
1763                    username: Some(String::from("orhun")),
1764                    pr_author: Some(String::from("author-2")),
1765                    pr_title: Some(String::from("2")),
1766                    pr_number: Some(66),
1767                    pr_numbers: vec![],
1768                    pr_labels: vec![String::from("rust")],
1769                    is_first_time: false,
1770                },
1771                remote: Some(RemoteContributor {
1772                    username: Some(String::from("orhun")),
1773                    pr_author: Some(String::from("author-2")),
1774                    pr_title: Some(String::from("2")),
1775                    pr_number: Some(66),
1776                    pr_numbers: vec![],
1777                    pr_labels: vec![String::from("rust")],
1778                    is_first_time: false,
1779                }),
1780                ..Default::default()
1781            },
1782            Commit {
1783                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1784                message: String::from("update metadata"),
1785                gitea: RemoteContributor {
1786                    username: Some(String::from("nuhro")),
1787                    pr_author: Some(String::from("author-3")),
1788                    pr_title: Some(String::from("3")),
1789                    pr_number: Some(53),
1790                    pr_numbers: vec![],
1791                    pr_labels: vec![String::from("deps")],
1792                    is_first_time: false,
1793                },
1794                remote: Some(RemoteContributor {
1795                    username: Some(String::from("nuhro")),
1796                    pr_author: Some(String::from("author-3")),
1797                    pr_title: Some(String::from("3")),
1798                    pr_number: Some(53),
1799                    pr_numbers: vec![],
1800                    pr_labels: vec![String::from("deps")],
1801                    is_first_time: false,
1802                }),
1803                ..Default::default()
1804            },
1805            Commit {
1806                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1807                message: String::from("do some stuff"),
1808                gitea: RemoteContributor {
1809                    username: Some(String::from("awesome_contributor")),
1810                    pr_author: Some(String::from("author-4")),
1811                    pr_title: Some(String::from("4")),
1812                    pr_number: Some(1_000),
1813                    pr_numbers: vec![],
1814                    pr_labels: vec![String::from("deps")],
1815                    is_first_time: false,
1816                },
1817                remote: Some(RemoteContributor {
1818                    username: Some(String::from("awesome_contributor")),
1819                    pr_author: Some(String::from("author-4")),
1820                    pr_title: Some(String::from("4")),
1821                    pr_number: Some(1_000),
1822                    pr_numbers: vec![],
1823                    pr_labels: vec![String::from("deps")],
1824                    is_first_time: false,
1825                }),
1826                ..Default::default()
1827            },
1828            Commit {
1829                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1830                message: String::from("alright"),
1831                gitea: RemoteContributor {
1832                    username: Some(String::from("orhun")),
1833                    pr_author: Some(String::from("author-5")),
1834                    pr_title: Some(String::from("5")),
1835                    pr_number: Some(999_999),
1836                    pr_numbers: vec![],
1837                    pr_labels: vec![String::from("github")],
1838                    is_first_time: false,
1839                },
1840                remote: Some(RemoteContributor {
1841                    username: Some(String::from("orhun")),
1842                    pr_author: Some(String::from("author-5")),
1843                    pr_title: Some(String::from("5")),
1844                    pr_number: Some(999_999),
1845                    pr_numbers: vec![],
1846                    pr_labels: vec![String::from("github")],
1847                    is_first_time: false,
1848                }),
1849                ..Default::default()
1850            },
1851            Commit {
1852                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1853                message: String::from("should be fine"),
1854                gitea: RemoteContributor {
1855                    username: Some(String::from("someone")),
1856                    pr_author: None,
1857                    pr_title: None,
1858                    pr_number: None,
1859                    pr_numbers: vec![],
1860                    pr_labels: vec![],
1861                    is_first_time: false,
1862                },
1863                remote: Some(RemoteContributor {
1864                    username: Some(String::from("someone")),
1865                    pr_author: None,
1866                    pr_title: None,
1867                    pr_number: None,
1868                    pr_numbers: vec![],
1869                    pr_labels: vec![],
1870                    is_first_time: false,
1871                }),
1872                ..Default::default()
1873            },
1874        ];
1875        assert_eq!(expected_commits, release.commits);
1876
1877        release.gitea.contributors.sort_by_key(|a| a.pr_number);
1878
1879        let expected_metadata = RemoteReleaseMetadata {
1880            contributors: vec![
1881                RemoteContributor {
1882                    username: Some(String::from("someone")),
1883                    pr_author: None,
1884                    pr_title: None,
1885                    pr_number: None,
1886                    pr_numbers: vec![],
1887                    pr_labels: vec![],
1888                    is_first_time: true,
1889                },
1890                RemoteContributor {
1891                    username: Some(String::from("orhun")),
1892                    pr_author: None,
1893                    pr_title: Some(String::from("1")),
1894                    pr_number: Some(42),
1895                    pr_numbers: vec![42, 66, 999_999],
1896                    pr_labels: vec![String::from("rust")],
1897                    is_first_time: true,
1898                },
1899                RemoteContributor {
1900                    username: Some(String::from("nuhro")),
1901                    pr_author: None,
1902                    pr_title: Some(String::from("3")),
1903                    pr_number: Some(53),
1904                    pr_numbers: vec![53],
1905                    pr_labels: vec![String::from("deps")],
1906                    is_first_time: true,
1907                },
1908                RemoteContributor {
1909                    username: Some(String::from("awesome_contributor")),
1910                    pr_author: None,
1911                    pr_title: Some(String::from("4")),
1912                    pr_number: Some(1_000),
1913                    pr_numbers: vec![1_000],
1914                    pr_labels: vec![String::from("deps")],
1915                    is_first_time: true,
1916                },
1917            ],
1918        };
1919        assert_eq!(expected_metadata, release.gitea);
1920
1921        Ok(())
1922    }
1923
1924    #[cfg(feature = "bitbucket")]
1925    #[test]
1926    fn update_bitbucket_metadata() -> Result<()> {
1927        use crate::remote::bitbucket::{
1928            BitbucketCommit, BitbucketCommitAuthor, BitbucketPullRequest,
1929            BitbucketPullRequestMergeCommit,
1930        };
1931
1932        let mut release = Release {
1933            version: None,
1934            message: None,
1935            extra: None,
1936            commits: vec![
1937                Commit::from(String::from(
1938                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add bitbucket integration",
1939                )),
1940                Commit::from(String::from(
1941                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix bitbucket integration",
1942                )),
1943                Commit::from(String::from(
1944                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
1945                )),
1946                Commit::from(String::from(
1947                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
1948                )),
1949                Commit::from(String::from(
1950                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
1951                )),
1952                Commit::from(String::from(
1953                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
1954                )),
1955            ],
1956            commit_range: None,
1957            commit_id: None,
1958            timestamp: None,
1959            previous: Some(Box::new(Release {
1960                version: Some(String::from("1.0.0")),
1961                ..Default::default()
1962            })),
1963            repository: Some(String::from("/root/repo")),
1964            submodule_commits: HashMap::new(),
1965            statistics: None,
1966            bump_type: None,
1967            #[cfg(feature = "github")]
1968            github: RemoteReleaseMetadata {
1969                contributors: vec![],
1970            },
1971            #[cfg(feature = "gitlab")]
1972            gitlab: RemoteReleaseMetadata {
1973                contributors: vec![],
1974            },
1975            #[cfg(feature = "gitea")]
1976            gitea: RemoteReleaseMetadata {
1977                contributors: vec![],
1978            },
1979            #[cfg(feature = "bitbucket")]
1980            bitbucket: RemoteReleaseMetadata {
1981                contributors: vec![],
1982            },
1983            #[cfg(feature = "azure_devops")]
1984            azure_devops: RemoteReleaseMetadata {
1985                contributors: vec![],
1986            },
1987        };
1988        release.update_bitbucket_metadata(
1989            vec![
1990                BitbucketCommit {
1991                    hash: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1992                    author: Some(BitbucketCommitAuthor {
1993                        login: Some(String::from("orhun")),
1994                        nickname: None,
1995                    }),
1996                    date: String::from("2021-07-18T15:14:39+03:00"),
1997                },
1998                BitbucketCommit {
1999                    hash: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
2000                    author: Some(BitbucketCommitAuthor {
2001                        login: Some(String::from("orhun")),
2002                        nickname: None,
2003                    }),
2004                    date: String::from("2021-07-18T15:12:19+03:00"),
2005                },
2006                BitbucketCommit {
2007                    hash: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
2008                    author: Some(BitbucketCommitAuthor {
2009                        login: Some(String::from("nuhro")),
2010                        nickname: None,
2011                    }),
2012                    date: String::from("2021-07-18T15:07:23+03:00"),
2013                },
2014                BitbucketCommit {
2015                    hash: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
2016                    author: Some(BitbucketCommitAuthor {
2017                        login: Some(String::from("awesome_contributor")),
2018                        nickname: None,
2019                    }),
2020                    date: String::from("2021-07-18T15:05:10+03:00"),
2021                },
2022                BitbucketCommit {
2023                    hash: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
2024                    author: Some(BitbucketCommitAuthor {
2025                        login: Some(String::from("orhun")),
2026                        nickname: None,
2027                    }),
2028                    date: String::from("2021-07-18T15:03:30+03:00"),
2029                },
2030                BitbucketCommit {
2031                    hash: String::from("6c34967147560ea09658776d4901709139b4ad66"),
2032                    author: Some(BitbucketCommitAuthor {
2033                        login: Some(String::from("someone")),
2034                        nickname: None,
2035                    }),
2036                    date: String::from("2021-07-18T15:00:38+03:00"),
2037                },
2038                BitbucketCommit {
2039                    hash: String::from("0c34967147560e809658776d4901709139b4ad68"),
2040                    author: Some(BitbucketCommitAuthor {
2041                        login: Some(String::from("idk")),
2042                        nickname: None,
2043                    }),
2044                    date: String::from("2021-07-18T15:00:01+03:00"),
2045                },
2046                BitbucketCommit {
2047                    hash: String::from("kk34967147560e809658776d4901709139b4ad68"),
2048                    author: Some(BitbucketCommitAuthor {
2049                        login: Some(String::from("orhun")),
2050                        nickname: None,
2051                    }),
2052                    date: String::from("2021-07-14T21:25:24+03:00"),
2053                },
2054            ]
2055            .into_iter()
2056            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
2057            .collect(),
2058            vec![Box::new(BitbucketPullRequest {
2059                id: 1,
2060                title: Some(String::from("1")),
2061                author: BitbucketCommitAuthor {
2062                    login: Some(String::from("42")),
2063                    nickname: Some(String::from("author-1")),
2064                },
2065                merge_commit: BitbucketPullRequestMergeCommit {
2066                    // Bitbucket merge commits returned in short format
2067                    hash: String::from("1d244937ee6c"),
2068                },
2069            })],
2070        )?;
2071        #[allow(deprecated)]
2072        let expected_commits = vec![
2073            Commit {
2074                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
2075                message: String::from("add bitbucket integration"),
2076                bitbucket: RemoteContributor {
2077                    username: Some(String::from("orhun")),
2078                    pr_author: Some(String::from("author-1")),
2079                    pr_title: Some(String::from("1")),
2080                    pr_number: Some(1),
2081                    pr_numbers: vec![],
2082                    pr_labels: vec![],
2083                    is_first_time: false,
2084                },
2085                remote: Some(RemoteContributor {
2086                    username: Some(String::from("orhun")),
2087                    pr_author: Some(String::from("author-1")),
2088                    pr_title: Some(String::from("1")),
2089                    pr_number: Some(1),
2090                    pr_numbers: vec![],
2091                    pr_labels: vec![],
2092                    is_first_time: false,
2093                }),
2094                ..Default::default()
2095            },
2096            Commit {
2097                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
2098                message: String::from("fix bitbucket integration"),
2099                bitbucket: RemoteContributor {
2100                    username: Some(String::from("orhun")),
2101                    pr_author: None,
2102                    pr_title: None,
2103                    pr_number: None,
2104                    pr_numbers: vec![],
2105                    pr_labels: vec![],
2106                    is_first_time: false,
2107                },
2108                remote: Some(RemoteContributor {
2109                    username: Some(String::from("orhun")),
2110                    pr_author: None,
2111                    pr_title: None,
2112                    pr_number: None,
2113                    pr_numbers: vec![],
2114                    pr_labels: vec![],
2115                    is_first_time: false,
2116                }),
2117                ..Default::default()
2118            },
2119            Commit {
2120                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
2121                message: String::from("update metadata"),
2122                bitbucket: RemoteContributor {
2123                    username: Some(String::from("nuhro")),
2124                    pr_author: None,
2125                    pr_title: None,
2126                    pr_number: None,
2127                    pr_numbers: vec![],
2128                    pr_labels: vec![],
2129                    is_first_time: false,
2130                },
2131                remote: Some(RemoteContributor {
2132                    username: Some(String::from("nuhro")),
2133                    pr_author: None,
2134                    pr_title: None,
2135                    pr_number: None,
2136                    pr_numbers: vec![],
2137                    pr_labels: vec![],
2138                    is_first_time: false,
2139                }),
2140                ..Default::default()
2141            },
2142            Commit {
2143                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
2144                message: String::from("do some stuff"),
2145                bitbucket: RemoteContributor {
2146                    username: Some(String::from("awesome_contributor")),
2147                    pr_author: None,
2148                    pr_title: None,
2149                    pr_number: None,
2150                    pr_numbers: vec![],
2151                    pr_labels: vec![],
2152                    is_first_time: false,
2153                },
2154                remote: Some(RemoteContributor {
2155                    username: Some(String::from("awesome_contributor")),
2156                    pr_author: None,
2157                    pr_title: None,
2158                    pr_number: None,
2159                    pr_numbers: vec![],
2160                    pr_labels: vec![],
2161                    is_first_time: false,
2162                }),
2163                ..Default::default()
2164            },
2165            Commit {
2166                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
2167                message: String::from("alright"),
2168                bitbucket: RemoteContributor {
2169                    username: Some(String::from("orhun")),
2170                    pr_author: None,
2171                    pr_title: None,
2172                    pr_number: None,
2173                    pr_numbers: vec![],
2174                    pr_labels: vec![],
2175                    is_first_time: false,
2176                },
2177                remote: Some(RemoteContributor {
2178                    username: Some(String::from("orhun")),
2179                    pr_author: None,
2180                    pr_title: None,
2181                    pr_number: None,
2182                    pr_numbers: vec![],
2183                    pr_labels: vec![],
2184                    is_first_time: false,
2185                }),
2186                ..Default::default()
2187            },
2188            Commit {
2189                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
2190                message: String::from("should be fine"),
2191                bitbucket: RemoteContributor {
2192                    username: Some(String::from("someone")),
2193                    pr_author: None,
2194                    pr_title: None,
2195                    pr_number: None,
2196                    pr_numbers: vec![],
2197                    pr_labels: vec![],
2198                    is_first_time: false,
2199                },
2200                remote: Some(RemoteContributor {
2201                    username: Some(String::from("someone")),
2202                    pr_author: None,
2203                    pr_title: None,
2204                    pr_number: None,
2205                    pr_numbers: vec![],
2206                    pr_labels: vec![],
2207                    is_first_time: false,
2208                }),
2209                ..Default::default()
2210            },
2211        ];
2212        assert_eq!(expected_commits, release.commits);
2213
2214        release.bitbucket.contributors.sort_by_key(|a| a.pr_number);
2215
2216        let expected_metadata = RemoteReleaseMetadata {
2217            contributors: vec![
2218                RemoteContributor {
2219                    username: Some(String::from("nuhro")),
2220                    pr_author: None,
2221                    pr_title: None,
2222                    pr_number: None,
2223                    pr_numbers: vec![],
2224                    pr_labels: vec![],
2225                    is_first_time: true,
2226                },
2227                RemoteContributor {
2228                    username: Some(String::from("awesome_contributor")),
2229                    pr_author: None,
2230                    pr_title: None,
2231                    pr_number: None,
2232                    pr_numbers: vec![],
2233                    pr_labels: vec![],
2234                    is_first_time: true,
2235                },
2236                RemoteContributor {
2237                    username: Some(String::from("someone")),
2238                    pr_author: None,
2239                    pr_title: None,
2240                    pr_number: None,
2241                    pr_numbers: vec![],
2242                    pr_labels: vec![],
2243                    is_first_time: true,
2244                },
2245                RemoteContributor {
2246                    username: Some(String::from("orhun")),
2247                    pr_author: None,
2248                    pr_title: Some(String::from("1")),
2249                    pr_number: Some(1),
2250                    pr_numbers: vec![1],
2251                    pr_labels: vec![],
2252                    is_first_time: false,
2253                },
2254            ],
2255        };
2256        assert_eq!(expected_metadata, release.bitbucket);
2257
2258        Ok(())
2259    }
2260
2261    #[cfg(feature = "azure_devops")]
2262    #[test]
2263    fn update_azure_devops_metadata() -> Result<()> {
2264        use crate::remote::azure_devops::{
2265            AzureDevOpsCommit, AzureDevOpsCommitAuthor, AzureDevOpsCommitRef,
2266            AzureDevOpsPullRequest, AzureDevOpsUser,
2267        };
2268
2269        let mut release = Release {
2270            version: None,
2271            message: None,
2272            extra: None,
2273            commits: vec![
2274                Commit::from(String::from(
2275                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add azure devops integration",
2276                )),
2277                Commit::from(String::from(
2278                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix azure devops integration",
2279                )),
2280                Commit::from(String::from(
2281                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
2282                )),
2283                Commit::from(String::from(
2284                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
2285                )),
2286                Commit::from(String::from(
2287                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
2288                )),
2289                Commit::from(String::from(
2290                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
2291                )),
2292            ],
2293            commit_range: None,
2294            commit_id: None,
2295            timestamp: None,
2296            previous: Some(Box::new(Release {
2297                version: Some(String::from("1.0.0")),
2298                ..Default::default()
2299            })),
2300            repository: Some(String::from("/root/repo")),
2301            submodule_commits: HashMap::new(),
2302            statistics: None,
2303            bump_type: None,
2304            #[cfg(feature = "github")]
2305            github: RemoteReleaseMetadata {
2306                contributors: vec![],
2307            },
2308            #[cfg(feature = "gitlab")]
2309            gitlab: RemoteReleaseMetadata {
2310                contributors: vec![],
2311            },
2312            #[cfg(feature = "gitea")]
2313            gitea: RemoteReleaseMetadata {
2314                contributors: vec![],
2315            },
2316            #[cfg(feature = "bitbucket")]
2317            bitbucket: RemoteReleaseMetadata {
2318                contributors: vec![],
2319            },
2320            #[cfg(feature = "azure_devops")]
2321            azure_devops: RemoteReleaseMetadata {
2322                contributors: vec![],
2323            },
2324        };
2325        release.update_azure_devops_metadata(
2326            vec![
2327                AzureDevOpsCommit {
2328                    commit_id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
2329                    author: Some(AzureDevOpsCommitAuthor {
2330                        name: Some(String::from("orhun")),
2331                        email: Some(String::from("orhun@example.com")),
2332                        date: Some(String::from("2021-07-18T15:14:39+03:00")),
2333                    }),
2334                    committer: None,
2335                },
2336                AzureDevOpsCommit {
2337                    commit_id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
2338                    author: Some(AzureDevOpsCommitAuthor {
2339                        name: Some(String::from("orhun")),
2340                        email: Some(String::from("orhun@example.com")),
2341                        date: Some(String::from("2021-07-18T15:12:19+03:00")),
2342                    }),
2343                    committer: None,
2344                },
2345                AzureDevOpsCommit {
2346                    commit_id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
2347                    author: Some(AzureDevOpsCommitAuthor {
2348                        name: Some(String::from("nuhro")),
2349                        email: Some(String::from("nuhro@example.com")),
2350                        date: Some(String::from("2021-07-18T15:07:23+03:00")),
2351                    }),
2352                    committer: None,
2353                },
2354                AzureDevOpsCommit {
2355                    commit_id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
2356                    author: Some(AzureDevOpsCommitAuthor {
2357                        name: Some(String::from("awesome_contributor")),
2358                        email: Some(String::from("awesome@example.com")),
2359                        date: Some(String::from("2021-07-18T15:05:10+03:00")),
2360                    }),
2361                    committer: None,
2362                },
2363                AzureDevOpsCommit {
2364                    commit_id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
2365                    author: Some(AzureDevOpsCommitAuthor {
2366                        name: Some(String::from("orhun")),
2367                        email: Some(String::from("orhun@example.com")),
2368                        date: Some(String::from("2021-07-18T15:03:30+03:00")),
2369                    }),
2370                    committer: None,
2371                },
2372                AzureDevOpsCommit {
2373                    commit_id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
2374                    author: Some(AzureDevOpsCommitAuthor {
2375                        name: Some(String::from("someone")),
2376                        email: Some(String::from("someone@example.com")),
2377                        date: Some(String::from("2021-07-18T15:00:38+03:00")),
2378                    }),
2379                    committer: None,
2380                },
2381                AzureDevOpsCommit {
2382                    commit_id: String::from("0c34967147560e809658776d4901709139b4ad68"),
2383                    author: Some(AzureDevOpsCommitAuthor {
2384                        name: Some(String::from("idk")),
2385                        email: Some(String::from("idk@example.com")),
2386                        date: Some(String::from("2021-07-18T15:00:01+03:00")),
2387                    }),
2388                    committer: None,
2389                },
2390                AzureDevOpsCommit {
2391                    commit_id: String::from("kk34967147560e809658776d4901709139b4ad68"),
2392                    author: Some(AzureDevOpsCommitAuthor {
2393                        name: Some(String::from("orhun")),
2394                        email: Some(String::from("orhun@example.com")),
2395                        date: Some(String::from("2021-07-14T21:25:24+03:00")),
2396                    }),
2397                    committer: None,
2398                },
2399            ]
2400            .into_iter()
2401            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
2402            .collect(),
2403            vec![Box::new(AzureDevOpsPullRequest {
2404                pull_request_id: 42,
2405                title: Some(String::from("1")),
2406                status: String::from("completed"),
2407                created_by: Some(AzureDevOpsUser {
2408                    display_name: Some(String::from("author-1")),
2409                    unique_name: None,
2410                }),
2411                last_merge_commit: Some(AzureDevOpsCommitRef {
2412                    commit_id: Some(String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071")),
2413                }),
2414                labels: vec![],
2415            })],
2416        )?;
2417        #[allow(deprecated)]
2418        let expected_commits = vec![
2419            Commit {
2420                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
2421                message: String::from("add azure devops integration"),
2422                azure_devops: RemoteContributor {
2423                    username: Some(String::from("orhun")),
2424                    pr_author: Some(String::from("author-1")),
2425                    pr_title: Some(String::from("1")),
2426                    pr_number: Some(42),
2427                    pr_numbers: vec![],
2428                    pr_labels: vec![],
2429                    is_first_time: false,
2430                },
2431                remote: Some(RemoteContributor {
2432                    username: Some(String::from("orhun")),
2433                    pr_author: Some(String::from("author-1")),
2434                    pr_title: Some(String::from("1")),
2435                    pr_number: Some(42),
2436                    pr_numbers: vec![],
2437                    pr_labels: vec![],
2438                    is_first_time: false,
2439                }),
2440                ..Default::default()
2441            },
2442            Commit {
2443                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
2444                message: String::from("fix azure devops integration"),
2445                azure_devops: RemoteContributor {
2446                    username: Some(String::from("orhun")),
2447                    pr_author: None,
2448                    pr_title: None,
2449                    pr_number: None,
2450                    pr_numbers: vec![],
2451                    pr_labels: vec![],
2452                    is_first_time: false,
2453                },
2454                remote: Some(RemoteContributor {
2455                    username: Some(String::from("orhun")),
2456                    pr_author: None,
2457                    pr_title: None,
2458                    pr_number: None,
2459                    pr_numbers: vec![],
2460                    pr_labels: vec![],
2461                    is_first_time: false,
2462                }),
2463                ..Default::default()
2464            },
2465            Commit {
2466                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
2467                message: String::from("update metadata"),
2468                azure_devops: RemoteContributor {
2469                    username: Some(String::from("nuhro")),
2470                    pr_author: None,
2471                    pr_title: None,
2472                    pr_number: None,
2473                    pr_numbers: vec![],
2474                    pr_labels: vec![],
2475                    is_first_time: false,
2476                },
2477                remote: Some(RemoteContributor {
2478                    username: Some(String::from("nuhro")),
2479                    pr_author: None,
2480                    pr_title: None,
2481                    pr_number: None,
2482                    pr_numbers: vec![],
2483                    pr_labels: vec![],
2484                    is_first_time: false,
2485                }),
2486                ..Default::default()
2487            },
2488            Commit {
2489                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
2490                message: String::from("do some stuff"),
2491                azure_devops: RemoteContributor {
2492                    username: Some(String::from("awesome_contributor")),
2493                    pr_author: None,
2494                    pr_title: None,
2495                    pr_number: None,
2496                    pr_numbers: vec![],
2497                    pr_labels: vec![],
2498                    is_first_time: false,
2499                },
2500                remote: Some(RemoteContributor {
2501                    username: Some(String::from("awesome_contributor")),
2502                    pr_author: None,
2503                    pr_title: None,
2504                    pr_number: None,
2505                    pr_numbers: vec![],
2506                    pr_labels: vec![],
2507                    is_first_time: false,
2508                }),
2509                ..Default::default()
2510            },
2511            Commit {
2512                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
2513                message: String::from("alright"),
2514                azure_devops: RemoteContributor {
2515                    username: Some(String::from("orhun")),
2516                    pr_author: None,
2517                    pr_title: None,
2518                    pr_number: None,
2519                    pr_numbers: vec![],
2520                    pr_labels: vec![],
2521                    is_first_time: false,
2522                },
2523                remote: Some(RemoteContributor {
2524                    username: Some(String::from("orhun")),
2525                    pr_author: None,
2526                    pr_title: None,
2527                    pr_number: None,
2528                    pr_numbers: vec![],
2529                    pr_labels: vec![],
2530                    is_first_time: false,
2531                }),
2532                ..Default::default()
2533            },
2534            Commit {
2535                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
2536                message: String::from("should be fine"),
2537                azure_devops: RemoteContributor {
2538                    username: Some(String::from("someone")),
2539                    pr_author: None,
2540                    pr_title: None,
2541                    pr_number: None,
2542                    pr_numbers: vec![],
2543                    pr_labels: vec![],
2544                    is_first_time: false,
2545                },
2546                remote: Some(RemoteContributor {
2547                    username: Some(String::from("someone")),
2548                    pr_author: None,
2549                    pr_title: None,
2550                    pr_number: None,
2551                    pr_numbers: vec![],
2552                    pr_labels: vec![],
2553                    is_first_time: false,
2554                }),
2555                ..Default::default()
2556            },
2557        ];
2558        assert_eq!(expected_commits, release.commits);
2559
2560        release
2561            .azure_devops
2562            .contributors
2563            .sort_by_key(|a| a.pr_number);
2564
2565        let expected_metadata = RemoteReleaseMetadata {
2566            contributors: vec![
2567                RemoteContributor {
2568                    username: Some(String::from("nuhro")),
2569                    pr_author: None,
2570                    pr_title: None,
2571                    pr_number: None,
2572                    pr_numbers: vec![],
2573                    pr_labels: vec![],
2574                    is_first_time: true,
2575                },
2576                RemoteContributor {
2577                    username: Some(String::from("awesome_contributor")),
2578                    pr_author: None,
2579                    pr_title: None,
2580                    pr_number: None,
2581                    pr_numbers: vec![],
2582                    pr_labels: vec![],
2583                    is_first_time: true,
2584                },
2585                RemoteContributor {
2586                    username: Some(String::from("someone")),
2587                    pr_author: None,
2588                    pr_title: None,
2589                    pr_number: None,
2590                    pr_numbers: vec![],
2591                    pr_labels: vec![],
2592                    is_first_time: true,
2593                },
2594                RemoteContributor {
2595                    username: Some(String::from("orhun")),
2596                    pr_author: None,
2597                    pr_title: Some(String::from("1")),
2598                    pr_number: Some(42),
2599                    pr_numbers: vec![42],
2600                    pr_labels: vec![],
2601                    is_first_time: false,
2602                },
2603            ],
2604        };
2605        assert_eq!(expected_metadata, release.azure_devops);
2606
2607        Ok(())
2608    }
2609}