git_cliff_core/
release.rs

1use std::collections::HashMap;
2
3use next_version::{NextVersion, 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    /// Contributors.
51    #[cfg(feature = "github")]
52    pub github: RemoteReleaseMetadata,
53    /// Contributors.
54    #[cfg(feature = "gitlab")]
55    pub gitlab: RemoteReleaseMetadata,
56    /// Contributors.
57    #[cfg(feature = "gitea")]
58    pub gitea: RemoteReleaseMetadata,
59    /// Contributors.
60    #[cfg(feature = "bitbucket")]
61    pub bitbucket: RemoteReleaseMetadata,
62}
63
64#[cfg(feature = "github")]
65crate::update_release_metadata!(github, update_github_metadata);
66
67#[cfg(feature = "gitlab")]
68crate::update_release_metadata!(gitlab, update_gitlab_metadata);
69
70#[cfg(feature = "gitea")]
71crate::update_release_metadata!(gitea, update_gitea_metadata);
72
73#[cfg(feature = "bitbucket")]
74crate::update_release_metadata!(bitbucket, update_bitbucket_metadata);
75
76impl Release<'_> {
77    /// Calculates the next version based on the commits.
78    ///
79    /// It uses the default bump version configuration to calculate the next
80    /// version.
81    pub fn calculate_next_version(&self) -> Result<String> {
82        self.calculate_next_version_with_config(&Bump::default())
83    }
84
85    /// Returns a new `Release` instance with aggregated statistics populated.
86    ///
87    /// This method computes various statistics from the release data and sets
88    /// the `statistics` field. It does not modify the original release but
89    /// returns a new instance with the computed statistics included.
90    pub fn with_statistics(mut self) -> Self {
91        self.statistics = Some((&self).into());
92        self
93    }
94
95    /// Calculates the next version based on the commits.
96    ///
97    /// It uses the given bump version configuration to calculate the next
98    /// version.
99    pub(super) fn calculate_next_version_with_config(&self, config: &Bump) -> Result<String> {
100        match self
101            .previous
102            .as_ref()
103            .and_then(|release| release.version.clone())
104        {
105            Some(version) => {
106                let mut semver = Version::parse(&version);
107                let mut prefix = None;
108                if semver.is_err() && version.split('.').count() >= 2 {
109                    let mut found_numeric = false;
110                    for (i, c) in version.char_indices() {
111                        if c.is_numeric() && !found_numeric {
112                            found_numeric = true;
113                            let version_prefix = version[..i].to_string();
114                            let remaining = version[i..].to_string();
115                            let version = Version::parse(&remaining);
116                            if version.is_ok() {
117                                semver = version;
118                                prefix = Some(version_prefix);
119                                break;
120                            }
121                        } else if !c.is_numeric() && found_numeric {
122                            found_numeric = false;
123                        }
124                    }
125                }
126                let mut next_version = VersionUpdater::new()
127                    .with_features_always_increment_minor(
128                        config.features_always_bump_minor.unwrap_or(true),
129                    )
130                    .with_breaking_always_increment_major(
131                        config.breaking_always_bump_major.unwrap_or(true),
132                    );
133                if let Some(custom_major_increment_regex) = &config.custom_major_increment_regex {
134                    next_version = next_version
135                        .with_custom_major_increment_regex(custom_major_increment_regex)?;
136                }
137                if let Some(custom_minor_increment_regex) = &config.custom_minor_increment_regex {
138                    next_version = next_version
139                        .with_custom_minor_increment_regex(custom_minor_increment_regex)?;
140                }
141                let next_version = if let Some(bump_type) = &config.bump_type {
142                    match bump_type {
143                        BumpType::Major => semver?.increment_major().to_string(),
144                        BumpType::Minor => semver?.increment_minor().to_string(),
145                        BumpType::Patch => semver?.increment_patch().to_string(),
146                    }
147                } else {
148                    next_version
149                        .increment(
150                            &semver?,
151                            self.commits
152                                .iter()
153                                .map(|commit| commit.message.trim_end().to_string())
154                                .collect::<Vec<String>>(),
155                        )
156                        .to_string()
157                };
158                if let Some(prefix) = prefix {
159                    Ok(format!("{prefix}{next_version}"))
160                } else {
161                    Ok(next_version)
162                }
163            }
164            None => Ok(config.get_initial_tag()),
165        }
166    }
167}
168
169/// Representation of a list of releases.
170#[derive(Serialize)]
171pub struct Releases<'a> {
172    /// Releases.
173    pub releases: &'a Vec<Release<'a>>,
174}
175
176impl Releases<'_> {
177    /// Returns the list of releases as JSON.
178    pub fn as_json(&self) -> Result<String> {
179        Ok(serde_json::to_string(self.releases)?)
180    }
181}
182
183#[cfg(test)]
184mod test {
185    use pretty_assertions::assert_eq;
186
187    use super::*;
188    #[test]
189    fn bump_version() -> Result<()> {
190        fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
191            Release {
192                version: None,
193                message: None,
194                extra: None,
195                commits: commits
196                    .iter()
197                    .map(|v| Commit::from(v.to_string()))
198                    .collect(),
199                commit_range: None,
200                commit_id: None,
201                timestamp: None,
202                previous: Some(Box::new(Release {
203                    version: Some(String::from(version)),
204                    ..Default::default()
205                })),
206                repository: Some(String::from("/root/repo")),
207                submodule_commits: HashMap::new(),
208                statistics: None,
209                #[cfg(feature = "github")]
210                github: crate::remote::RemoteReleaseMetadata {
211                    contributors: vec![],
212                },
213                #[cfg(feature = "gitlab")]
214                gitlab: crate::remote::RemoteReleaseMetadata {
215                    contributors: vec![],
216                },
217                #[cfg(feature = "gitea")]
218                gitea: crate::remote::RemoteReleaseMetadata {
219                    contributors: vec![],
220                },
221                #[cfg(feature = "bitbucket")]
222                bitbucket: crate::remote::RemoteReleaseMetadata {
223                    contributors: vec![],
224                },
225            }
226        }
227
228        let test_shared = [
229            ("1.0.0", "1.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
230            ("1.0.0", "1.0.1", vec!["fix: add xyz", "fix: aaaaaa"]),
231            ("1.0.0", "2.0.0", vec!["feat!: add xyz", "feat: zzz"]),
232            ("1.0.0", "2.0.0", vec!["feat!: add xyz\n", "feat: zzz\n"]),
233            ("2.0.0", "2.0.1", vec!["fix: something"]),
234            ("foo/1.0.0", "foo/1.1.0", vec![
235                "feat: add xyz",
236                "fix: fix xyz",
237            ]),
238            ("bar/1.0.0", "bar/2.0.0", vec![
239                "fix: add xyz",
240                "fix!: aaaaaa",
241            ]),
242            ("zzz-123/test/1.0.0", "zzz-123/test/1.0.1", vec![
243                "fix: aaaaaa",
244            ]),
245            ("v100.0.0", "v101.0.0", vec!["feat!: something"]),
246            ("v1.0.0-alpha.1", "v1.0.0-alpha.2", vec!["fix: minor"]),
247            ("testing/v1.0.0-beta.1", "testing/v1.0.0-beta.2", vec![
248                "feat: nice",
249            ]),
250            ("tauri-v1.5.4", "tauri-v1.6.0", vec!["feat: something"]),
251            (
252                "rocket/rocket-v4.0.0-rc.1",
253                "rocket/rocket-v4.0.0-rc.2",
254                vec!["chore!: wow"],
255            ),
256            (
257                "aaa#/@#$@9384!#%^#@#@!#!239432413-idk-9999.2200.5932-alpha.419",
258                "aaa#/@#$@9384!#%^#@#@!#!239432413-idk-9999.2200.5932-alpha.420",
259                vec!["feat: damn this is working"],
260            ),
261        ];
262
263        for (version, expected_version, commits) in test_shared.iter().chain(
264            [
265                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
266                ("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
267                ("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
268                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
269                ("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]),
270                ("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
271            ]
272            .iter(),
273        ) {
274            let release = build_release(version, commits);
275            let next_version = release.calculate_next_version()?;
276            assert_eq!(expected_version, &next_version);
277            let next_version = release.calculate_next_version_with_config(&Bump::default())?;
278            assert_eq!(expected_version, &next_version);
279        }
280
281        for (version, expected_version, commits) in test_shared.iter().chain(
282            [
283                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
284                ("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]),
285                ("0.0.1", "0.0.2", vec!["feat!: add xyz", "feat: zzz"]),
286                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
287                ("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]),
288                ("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]),
289            ]
290            .iter(),
291        ) {
292            let release = build_release(version, commits);
293            let next_version = release.calculate_next_version_with_config(&Bump {
294                features_always_bump_minor: Some(false),
295                breaking_always_bump_major: Some(false),
296                initial_tag: None,
297                custom_major_increment_regex: None,
298                custom_minor_increment_regex: None,
299                bump_type: None,
300            })?;
301            assert_eq!(expected_version, &next_version);
302        }
303
304        for (version, expected_version, commits) in test_shared.iter().chain(
305            [
306                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
307                ("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
308                ("0.0.1", "0.1.0", vec!["feat!: add xyz", "feat: zzz"]),
309                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
310                ("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]),
311                ("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]),
312            ]
313            .iter(),
314        ) {
315            let release = build_release(version, commits);
316            let next_version = release.calculate_next_version_with_config(&Bump {
317                features_always_bump_minor: Some(true),
318                breaking_always_bump_major: Some(false),
319                initial_tag: None,
320                custom_major_increment_regex: None,
321                custom_minor_increment_regex: None,
322                bump_type: None,
323            })?;
324            assert_eq!(expected_version, &next_version);
325        }
326
327        for (version, expected_version, commits) in test_shared.iter().chain(
328            [
329                ("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
330                ("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]),
331                ("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
332                ("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
333                ("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]),
334                ("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
335            ]
336            .iter(),
337        ) {
338            let release = build_release(version, commits);
339            let next_version = release.calculate_next_version_with_config(&Bump {
340                features_always_bump_minor: Some(false),
341                breaking_always_bump_major: Some(true),
342                initial_tag: None,
343                custom_major_increment_regex: None,
344                custom_minor_increment_regex: None,
345                bump_type: None,
346            })?;
347            assert_eq!(expected_version, &next_version);
348        }
349
350        let empty_release = Release {
351            previous: Some(Box::new(Release {
352                version: None,
353                ..Default::default()
354            })),
355            ..Default::default()
356        };
357        assert_eq!("0.1.0", empty_release.calculate_next_version()?);
358        for (features_always_bump_minor, breaking_always_bump_major) in
359            [(true, true), (true, false), (false, true), (false, false)]
360        {
361            assert_eq!(
362                "0.1.0",
363                empty_release.calculate_next_version_with_config(&Bump {
364                    features_always_bump_minor: Some(features_always_bump_minor),
365                    breaking_always_bump_major: Some(breaking_always_bump_major),
366                    initial_tag: None,
367                    custom_major_increment_regex: None,
368                    custom_minor_increment_regex: None,
369                    bump_type: None,
370                })?
371            );
372        }
373        Ok(())
374    }
375
376    #[test]
377    fn with_statistics() -> Result<()> {
378        let release = Release {
379            commits: vec![],
380            timestamp: Some(1649373910),
381            previous: Some(Box::new(Release {
382                timestamp: Some(1649201110),
383                ..Default::default()
384            })),
385            repository: Some(String::from("/root/repo")),
386            ..Default::default()
387        };
388
389        assert!(release.statistics.is_none());
390        let release = release.with_statistics();
391        assert!(release.statistics.is_some());
392
393        Ok(())
394    }
395
396    #[cfg(feature = "github")]
397    #[test]
398    fn update_github_metadata() -> Result<()> {
399        use crate::remote::github::{
400            GitHubCommit, GitHubCommitAuthor, GitHubCommitDetails, GitHubCommitDetailsAuthor,
401            GitHubPullRequest, PullRequestLabel,
402        };
403
404        let mut release = Release {
405            version: None,
406            message: None,
407            extra: None,
408            commits: vec![
409                Commit::from(String::from(
410                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github integration",
411                )),
412                Commit::from(String::from(
413                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix github integration",
414                )),
415                Commit::from(String::from(
416                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
417                )),
418                Commit::from(String::from(
419                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
420                )),
421                Commit::from(String::from(
422                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
423                )),
424                Commit::from(String::from(
425                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
426                )),
427            ],
428            commit_range: None,
429            commit_id: None,
430            timestamp: None,
431            previous: Some(Box::new(Release {
432                version: Some(String::from("1.0.0")),
433                ..Default::default()
434            })),
435            repository: Some(String::from("/root/repo")),
436            submodule_commits: HashMap::new(),
437            statistics: None,
438            github: RemoteReleaseMetadata {
439                contributors: vec![],
440            },
441            #[cfg(feature = "gitlab")]
442            gitlab: RemoteReleaseMetadata {
443                contributors: vec![],
444            },
445            #[cfg(feature = "gitea")]
446            gitea: RemoteReleaseMetadata {
447                contributors: vec![],
448            },
449            #[cfg(feature = "bitbucket")]
450            bitbucket: RemoteReleaseMetadata {
451                contributors: vec![],
452            },
453        };
454        release.update_github_metadata(
455            vec![
456                GitHubCommit {
457                    sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
458                    author: Some(GitHubCommitAuthor {
459                        login: Some(String::from("orhun")),
460                    }),
461                    commit: Some(GitHubCommitDetails {
462                        author: GitHubCommitDetailsAuthor {
463                            date: String::from("2021-07-18T15:14:39+03:00"),
464                        },
465                    }),
466                },
467                GitHubCommit {
468                    sha: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
469                    author: Some(GitHubCommitAuthor {
470                        login: Some(String::from("orhun")),
471                    }),
472                    commit: Some(GitHubCommitDetails {
473                        author: GitHubCommitDetailsAuthor {
474                            date: String::from("2021-07-18T15:12:19+03:00"),
475                        },
476                    }),
477                },
478                GitHubCommit {
479                    sha: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
480                    author: Some(GitHubCommitAuthor {
481                        login: Some(String::from("nuhro")),
482                    }),
483                    commit: Some(GitHubCommitDetails {
484                        author: GitHubCommitDetailsAuthor {
485                            date: String::from("2021-07-18T15:07:23+03:00"),
486                        },
487                    }),
488                },
489                GitHubCommit {
490                    sha: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
491                    author: Some(GitHubCommitAuthor {
492                        login: Some(String::from("awesome_contributor")),
493                    }),
494                    commit: Some(GitHubCommitDetails {
495                        author: GitHubCommitDetailsAuthor {
496                            date: String::from("2021-07-18T15:05:10+03:00"),
497                        },
498                    }),
499                },
500                GitHubCommit {
501                    sha: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
502                    author: Some(GitHubCommitAuthor {
503                        login: Some(String::from("orhun")),
504                    }),
505                    commit: Some(GitHubCommitDetails {
506                        author: GitHubCommitDetailsAuthor {
507                            date: String::from("2021-07-18T15:03:30+03:00"),
508                        },
509                    }),
510                },
511                GitHubCommit {
512                    sha: String::from("6c34967147560ea09658776d4901709139b4ad66"),
513                    author: Some(GitHubCommitAuthor {
514                        login: Some(String::from("someone")),
515                    }),
516                    commit: Some(GitHubCommitDetails {
517                        author: GitHubCommitDetailsAuthor {
518                            date: String::from("2021-07-18T15:00:38+03:00"),
519                        },
520                    }),
521                },
522                GitHubCommit {
523                    sha: String::from("0c34967147560e809658776d4901709139b4ad68"),
524                    author: Some(GitHubCommitAuthor {
525                        login: Some(String::from("idk")),
526                    }),
527                    commit: Some(GitHubCommitDetails {
528                        author: GitHubCommitDetailsAuthor {
529                            date: String::from("2021-07-18T15:00:38+03:00"),
530                        },
531                    }),
532                },
533                GitHubCommit {
534                    sha: String::from("kk34967147560e809658776d4901709139b4ad68"),
535                    author: None,
536                    commit: None,
537                },
538                GitHubCommit {
539                    sha: String::new(),
540                    author: None,
541                    commit: None,
542                },
543            ]
544            .into_iter()
545            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
546            .collect(),
547            vec![
548                GitHubPullRequest {
549                    title: Some(String::from("1")),
550                    number: 42,
551                    merge_commit_sha: Some(String::from(
552                        "1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
553                    )),
554                    labels: vec![PullRequestLabel {
555                        name: String::from("rust"),
556                    }],
557                },
558                GitHubPullRequest {
559                    title: Some(String::from("2")),
560                    number: 66,
561                    merge_commit_sha: Some(String::from(
562                        "21f6aa587fcb772de13f2fde0e92697c51f84162",
563                    )),
564                    labels: vec![PullRequestLabel {
565                        name: String::from("rust"),
566                    }],
567                },
568                GitHubPullRequest {
569                    title: Some(String::from("3")),
570                    number: 53,
571                    merge_commit_sha: Some(String::from(
572                        "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
573                    )),
574                    labels: vec![PullRequestLabel {
575                        name: String::from("deps"),
576                    }],
577                },
578                GitHubPullRequest {
579                    title: Some(String::from("4")),
580                    number: 1000,
581                    merge_commit_sha: Some(String::from(
582                        "4d3ffe4753b923f4d7807c490e650e6624a12074",
583                    )),
584                    labels: vec![PullRequestLabel {
585                        name: String::from("deps"),
586                    }],
587                },
588                GitHubPullRequest {
589                    title: Some(String::from("5")),
590                    number: 999999,
591                    merge_commit_sha: Some(String::from(
592                        "5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
593                    )),
594                    labels: vec![PullRequestLabel {
595                        name: String::from("github"),
596                    }],
597                },
598            ]
599            .into_iter()
600            .map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
601            .collect(),
602        )?;
603        #[allow(deprecated)]
604        let expected_commits = vec![
605            Commit {
606                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
607                message: String::from("add github integration"),
608                github: RemoteContributor {
609                    username: Some(String::from("orhun")),
610                    pr_title: Some(String::from("1")),
611                    pr_number: Some(42),
612                    pr_labels: vec![String::from("rust")],
613                    is_first_time: false,
614                },
615                remote: Some(RemoteContributor {
616                    username: Some(String::from("orhun")),
617                    pr_title: Some(String::from("1")),
618                    pr_number: Some(42),
619                    pr_labels: vec![String::from("rust")],
620                    is_first_time: false,
621                }),
622                ..Default::default()
623            },
624            Commit {
625                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
626                message: String::from("fix github integration"),
627                github: RemoteContributor {
628                    username: Some(String::from("orhun")),
629                    pr_title: Some(String::from("2")),
630                    pr_number: Some(66),
631                    pr_labels: vec![String::from("rust")],
632                    is_first_time: false,
633                },
634                remote: Some(RemoteContributor {
635                    username: Some(String::from("orhun")),
636                    pr_title: Some(String::from("2")),
637                    pr_number: Some(66),
638                    pr_labels: vec![String::from("rust")],
639                    is_first_time: false,
640                }),
641                ..Default::default()
642            },
643            Commit {
644                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
645                message: String::from("update metadata"),
646                github: RemoteContributor {
647                    username: Some(String::from("nuhro")),
648                    pr_title: Some(String::from("3")),
649                    pr_number: Some(53),
650                    pr_labels: vec![String::from("deps")],
651                    is_first_time: false,
652                },
653                remote: Some(RemoteContributor {
654                    username: Some(String::from("nuhro")),
655                    pr_title: Some(String::from("3")),
656                    pr_number: Some(53),
657                    pr_labels: vec![String::from("deps")],
658                    is_first_time: false,
659                }),
660                ..Default::default()
661            },
662            Commit {
663                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
664                message: String::from("do some stuff"),
665                github: RemoteContributor {
666                    username: Some(String::from("awesome_contributor")),
667                    pr_title: Some(String::from("4")),
668                    pr_number: Some(1000),
669                    pr_labels: vec![String::from("deps")],
670                    is_first_time: false,
671                },
672                remote: Some(RemoteContributor {
673                    username: Some(String::from("awesome_contributor")),
674                    pr_title: Some(String::from("4")),
675                    pr_number: Some(1000),
676                    pr_labels: vec![String::from("deps")],
677                    is_first_time: false,
678                }),
679                ..Default::default()
680            },
681            Commit {
682                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
683                message: String::from("alright"),
684                github: RemoteContributor {
685                    username: Some(String::from("orhun")),
686                    pr_title: Some(String::from("5")),
687                    pr_number: Some(999999),
688                    pr_labels: vec![String::from("github")],
689                    is_first_time: false,
690                },
691                remote: Some(RemoteContributor {
692                    username: Some(String::from("orhun")),
693                    pr_title: Some(String::from("5")),
694                    pr_number: Some(999999),
695                    pr_labels: vec![String::from("github")],
696                    is_first_time: false,
697                }),
698                ..Default::default()
699            },
700            Commit {
701                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
702                message: String::from("should be fine"),
703                github: RemoteContributor {
704                    username: Some(String::from("someone")),
705                    pr_title: None,
706                    pr_number: None,
707                    pr_labels: vec![],
708                    is_first_time: false,
709                },
710                remote: Some(RemoteContributor {
711                    username: Some(String::from("someone")),
712                    pr_title: None,
713                    pr_number: None,
714                    pr_labels: vec![],
715                    is_first_time: false,
716                }),
717                ..Default::default()
718            },
719        ];
720        assert_eq!(expected_commits, release.commits);
721
722        release
723            .github
724            .contributors
725            .sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
726
727        let expected_metadata = RemoteReleaseMetadata {
728            contributors: vec![
729                RemoteContributor {
730                    username: Some(String::from("someone")),
731                    pr_title: None,
732                    pr_number: None,
733                    pr_labels: vec![],
734                    is_first_time: true,
735                },
736                RemoteContributor {
737                    username: Some(String::from("orhun")),
738                    pr_title: Some(String::from("1")),
739                    pr_number: Some(42),
740                    pr_labels: vec![String::from("rust")],
741                    is_first_time: true,
742                },
743                RemoteContributor {
744                    username: Some(String::from("nuhro")),
745                    pr_title: Some(String::from("3")),
746                    pr_number: Some(53),
747                    pr_labels: vec![String::from("deps")],
748                    is_first_time: true,
749                },
750                RemoteContributor {
751                    username: Some(String::from("awesome_contributor")),
752                    pr_title: Some(String::from("4")),
753                    pr_number: Some(1000),
754                    pr_labels: vec![String::from("deps")],
755                    is_first_time: true,
756                },
757            ],
758        };
759        assert_eq!(expected_metadata, release.github);
760
761        Ok(())
762    }
763
764    #[cfg(feature = "gitlab")]
765    #[test]
766    fn update_gitlab_metadata() -> Result<()> {
767        use crate::remote::gitlab::{GitLabCommit, GitLabMergeRequest, GitLabUser};
768
769        let mut release = Release {
770            version: None,
771            message: None,
772            extra: None,
773            commits: vec![
774                Commit::from(String::from(
775                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github integration",
776                )),
777                Commit::from(String::from(
778                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix github integration",
779                )),
780                Commit::from(String::from(
781                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
782                )),
783                Commit::from(String::from(
784                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
785                )),
786                Commit::from(String::from(
787                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
788                )),
789                Commit::from(String::from(
790                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
791                )),
792            ],
793            commit_range: None,
794            commit_id: None,
795            timestamp: None,
796            previous: Some(Box::new(Release {
797                version: Some(String::from("1.0.0")),
798                ..Default::default()
799            })),
800            repository: Some(String::from("/root/repo")),
801            submodule_commits: HashMap::new(),
802            statistics: None,
803            #[cfg(feature = "github")]
804            github: RemoteReleaseMetadata {
805                contributors: vec![],
806            },
807            #[cfg(feature = "gitlab")]
808            gitlab: RemoteReleaseMetadata {
809                contributors: vec![],
810            },
811            #[cfg(feature = "gitea")]
812            gitea: RemoteReleaseMetadata {
813                contributors: vec![],
814            },
815            #[cfg(feature = "bitbucket")]
816            bitbucket: RemoteReleaseMetadata {
817                contributors: vec![],
818            },
819        };
820        release.update_gitlab_metadata(
821            vec![
822                GitLabCommit {
823                    id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
824                    author_name: String::from("orhun"),
825                    short_id: String::from(""),
826                    title: String::from(""),
827                    author_email: String::from(""),
828                    authored_date: String::from(""),
829                    committer_name: String::from(""),
830                    committer_email: String::from(""),
831                    committed_date: String::from(""),
832                    created_at: String::from(""),
833                    message: String::from(""),
834                    parent_ids: vec![],
835                    web_url: String::from(""),
836                },
837                GitLabCommit {
838                    id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
839                    author_name: String::from("orhun"),
840                    short_id: String::from(""),
841                    title: String::from(""),
842                    author_email: String::from(""),
843                    authored_date: String::from(""),
844                    committer_name: String::from(""),
845                    committer_email: String::from(""),
846                    committed_date: String::from(""),
847                    created_at: String::from(""),
848                    message: String::from(""),
849                    parent_ids: vec![],
850                    web_url: String::from(""),
851                },
852                GitLabCommit {
853                    id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
854                    author_name: String::from("nuhro"),
855                    short_id: String::from(""),
856                    title: String::from(""),
857                    author_email: String::from(""),
858                    authored_date: String::from(""),
859                    committer_name: String::from(""),
860                    committer_email: String::from(""),
861                    committed_date: String::from(""),
862                    created_at: String::from(""),
863                    message: String::from(""),
864                    parent_ids: vec![],
865                    web_url: String::from(""),
866                },
867                GitLabCommit {
868                    id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
869                    author_name: String::from("awesome_contributor"),
870                    short_id: String::from(""),
871                    title: String::from(""),
872                    author_email: String::from(""),
873                    authored_date: String::from(""),
874                    committer_name: String::from(""),
875                    committer_email: String::from(""),
876                    committed_date: String::from(""),
877                    created_at: String::from(""),
878                    message: String::from(""),
879                    parent_ids: vec![],
880                    web_url: String::from(""),
881                },
882                GitLabCommit {
883                    id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
884                    author_name: String::from("orhun"),
885                    short_id: String::from(""),
886                    title: String::from(""),
887                    author_email: String::from(""),
888                    authored_date: String::from(""),
889                    committer_name: String::from(""),
890                    committer_email: String::from(""),
891                    committed_date: String::from(""),
892                    created_at: String::from(""),
893                    message: String::from(""),
894                    parent_ids: vec![],
895                    web_url: String::from(""),
896                },
897                GitLabCommit {
898                    id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
899                    author_name: String::from("someone"),
900                    short_id: String::from(""),
901                    title: String::from(""),
902                    author_email: String::from(""),
903                    authored_date: String::from(""),
904                    committer_name: String::from(""),
905                    committer_email: String::from(""),
906                    committed_date: String::from(""),
907                    created_at: String::from(""),
908                    message: String::from(""),
909                    parent_ids: vec![],
910                    web_url: String::from(""),
911                },
912                GitLabCommit {
913                    id: String::from("0c34967147560e809658776d4901709139b4ad68"),
914                    author_name: String::from("idk"),
915                    short_id: String::from(""),
916                    title: String::from(""),
917                    author_email: String::from(""),
918                    authored_date: String::from(""),
919                    committer_name: String::from(""),
920                    committer_email: String::from(""),
921                    committed_date: String::from(""),
922                    created_at: String::from(""),
923                    message: String::from(""),
924                    parent_ids: vec![],
925                    web_url: String::from(""),
926                },
927                GitLabCommit {
928                    id: String::from("kk34967147560e809658776d4901709139b4ad68"),
929                    author_name: String::from("orhun"),
930                    short_id: String::from(""),
931                    title: String::from(""),
932                    author_email: String::from(""),
933                    authored_date: String::from(""),
934                    committer_name: String::from(""),
935                    committer_email: String::from(""),
936                    committed_date: String::from(""),
937                    created_at: String::from(""),
938                    message: String::from(""),
939                    parent_ids: vec![],
940                    web_url: String::from(""),
941                },
942            ]
943            .into_iter()
944            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
945            .collect(),
946            vec![Box::new(GitLabMergeRequest {
947                title: String::from("1"),
948                merge_commit_sha: Some(String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071")),
949                id: 1,
950                iid: 1,
951                project_id: 1,
952                description: String::from(""),
953                state: String::from(""),
954                created_at: String::from(""),
955                author: GitLabUser {
956                    id: 1,
957                    name: String::from("42"),
958                    username: String::from("42"),
959                    state: String::from("42"),
960                    avatar_url: None,
961                    web_url: String::from("42"),
962                },
963                sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
964                web_url: String::from(""),
965                squash_commit_sha: None,
966                labels: vec![String::from("rust")],
967            })],
968        )?;
969        #[allow(deprecated)]
970        let expected_commits = vec![
971            Commit {
972                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
973                message: String::from("add github integration"),
974                gitlab: RemoteContributor {
975                    username: Some(String::from("orhun")),
976                    pr_title: Some(String::from("1")),
977                    pr_number: Some(1),
978                    pr_labels: vec![String::from("rust")],
979                    is_first_time: false,
980                },
981                remote: Some(RemoteContributor {
982                    username: Some(String::from("orhun")),
983                    pr_title: Some(String::from("1")),
984                    pr_number: Some(1),
985                    pr_labels: vec![String::from("rust")],
986                    is_first_time: false,
987                }),
988                ..Default::default()
989            },
990            Commit {
991                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
992                message: String::from("fix github integration"),
993                gitlab: RemoteContributor {
994                    username: Some(String::from("orhun")),
995                    pr_title: None,
996                    pr_number: None,
997                    pr_labels: vec![],
998                    is_first_time: false,
999                },
1000                remote: Some(RemoteContributor {
1001                    username: Some(String::from("orhun")),
1002                    pr_title: None,
1003                    pr_number: None,
1004                    pr_labels: vec![],
1005                    is_first_time: false,
1006                }),
1007                ..Default::default()
1008            },
1009            Commit {
1010                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1011                message: String::from("update metadata"),
1012                gitlab: RemoteContributor {
1013                    username: Some(String::from("nuhro")),
1014                    pr_title: None,
1015                    pr_number: None,
1016                    pr_labels: vec![],
1017                    is_first_time: false,
1018                },
1019                remote: Some(RemoteContributor {
1020                    username: Some(String::from("nuhro")),
1021                    pr_title: None,
1022                    pr_number: None,
1023                    pr_labels: vec![],
1024                    is_first_time: false,
1025                }),
1026                ..Default::default()
1027            },
1028            Commit {
1029                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1030                message: String::from("do some stuff"),
1031                gitlab: RemoteContributor {
1032                    username: Some(String::from("awesome_contributor")),
1033                    pr_title: None,
1034                    pr_number: None,
1035                    pr_labels: vec![],
1036                    is_first_time: false,
1037                },
1038                remote: Some(RemoteContributor {
1039                    username: Some(String::from("awesome_contributor")),
1040                    pr_title: None,
1041                    pr_number: None,
1042                    pr_labels: vec![],
1043                    is_first_time: false,
1044                }),
1045                ..Default::default()
1046            },
1047            Commit {
1048                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1049                message: String::from("alright"),
1050                gitlab: RemoteContributor {
1051                    username: Some(String::from("orhun")),
1052                    pr_title: None,
1053                    pr_number: None,
1054                    pr_labels: vec![],
1055                    is_first_time: false,
1056                },
1057                remote: Some(RemoteContributor {
1058                    username: Some(String::from("orhun")),
1059                    pr_title: None,
1060                    pr_number: None,
1061                    pr_labels: vec![],
1062                    is_first_time: false,
1063                }),
1064                ..Default::default()
1065            },
1066            Commit {
1067                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1068                message: String::from("should be fine"),
1069                gitlab: RemoteContributor {
1070                    username: Some(String::from("someone")),
1071                    pr_title: None,
1072                    pr_number: None,
1073                    pr_labels: vec![],
1074                    is_first_time: false,
1075                },
1076                remote: Some(RemoteContributor {
1077                    username: Some(String::from("someone")),
1078                    pr_title: None,
1079                    pr_number: None,
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
1089            .github
1090            .contributors
1091            .sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
1092
1093        let expected_metadata = RemoteReleaseMetadata {
1094            contributors: vec![
1095                RemoteContributor {
1096                    username: Some(String::from("orhun")),
1097                    pr_title: Some(String::from("1")),
1098                    pr_number: Some(1),
1099                    pr_labels: vec![String::from("rust")],
1100                    is_first_time: false,
1101                },
1102                RemoteContributor {
1103                    username: Some(String::from("nuhro")),
1104                    pr_title: None,
1105                    pr_number: None,
1106                    pr_labels: vec![],
1107                    is_first_time: true,
1108                },
1109                RemoteContributor {
1110                    username: Some(String::from("awesome_contributor")),
1111                    pr_title: None,
1112                    pr_number: None,
1113                    pr_labels: vec![],
1114                    is_first_time: true,
1115                },
1116                RemoteContributor {
1117                    username: Some(String::from("someone")),
1118                    pr_title: None,
1119                    pr_number: None,
1120                    pr_labels: vec![],
1121                    is_first_time: true,
1122                },
1123            ],
1124        };
1125        assert_eq!(expected_metadata, release.gitlab);
1126
1127        Ok(())
1128    }
1129
1130    #[cfg(feature = "gitea")]
1131    #[test]
1132    fn update_gitea_metadata() -> Result<()> {
1133        use crate::remote::gitea::{
1134            GiteaCommit, GiteaCommitAuthor, GiteaPullRequest, PullRequestLabel,
1135        };
1136
1137        let mut release = Release {
1138            version: None,
1139            message: None,
1140            extra: None,
1141            commits: vec![
1142                Commit::from(String::from(
1143                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github integration",
1144                )),
1145                Commit::from(String::from(
1146                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix github integration",
1147                )),
1148                Commit::from(String::from(
1149                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
1150                )),
1151                Commit::from(String::from(
1152                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
1153                )),
1154                Commit::from(String::from(
1155                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
1156                )),
1157                Commit::from(String::from(
1158                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
1159                )),
1160            ],
1161            commit_range: None,
1162            commit_id: None,
1163            timestamp: None,
1164            previous: Some(Box::new(Release {
1165                version: Some(String::from("1.0.0")),
1166                ..Default::default()
1167            })),
1168            repository: Some(String::from("/root/repo")),
1169            submodule_commits: HashMap::new(),
1170            statistics: None,
1171            #[cfg(feature = "github")]
1172            github: RemoteReleaseMetadata {
1173                contributors: vec![],
1174            },
1175            #[cfg(feature = "gitlab")]
1176            gitlab: RemoteReleaseMetadata {
1177                contributors: vec![],
1178            },
1179            #[cfg(feature = "gitea")]
1180            gitea: RemoteReleaseMetadata {
1181                contributors: vec![],
1182            },
1183            #[cfg(feature = "bitbucket")]
1184            bitbucket: RemoteReleaseMetadata {
1185                contributors: vec![],
1186            },
1187        };
1188        release.update_gitea_metadata(
1189            vec![
1190                GiteaCommit {
1191                    sha: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1192                    author: Some(GiteaCommitAuthor {
1193                        login: Some(String::from("orhun")),
1194                    }),
1195                    created: String::from("2021-07-18T15:14:39+03:00"),
1196                },
1197                GiteaCommit {
1198                    sha: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1199                    author: Some(GiteaCommitAuthor {
1200                        login: Some(String::from("orhun")),
1201                    }),
1202                    created: String::from("2021-07-18T15:12:19+03:00"),
1203                },
1204                GiteaCommit {
1205                    sha: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1206                    author: Some(GiteaCommitAuthor {
1207                        login: Some(String::from("nuhro")),
1208                    }),
1209                    created: String::from("2021-07-18T15:07:23+03:00"),
1210                },
1211                GiteaCommit {
1212                    sha: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1213                    author: Some(GiteaCommitAuthor {
1214                        login: Some(String::from("awesome_contributor")),
1215                    }),
1216                    created: String::from("2021-07-18T15:05:10+03:00"),
1217                },
1218                GiteaCommit {
1219                    sha: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1220                    author: Some(GiteaCommitAuthor {
1221                        login: Some(String::from("orhun")),
1222                    }),
1223                    created: String::from("2021-07-18T15:03:30+03:00"),
1224                },
1225                GiteaCommit {
1226                    sha: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1227                    author: Some(GiteaCommitAuthor {
1228                        login: Some(String::from("someone")),
1229                    }),
1230                    created: String::from("2021-07-18T15:00:38+03:00"),
1231                },
1232                GiteaCommit {
1233                    sha: String::from("0c34967147560e809658776d4901709139b4ad68"),
1234                    author: Some(GiteaCommitAuthor {
1235                        login: Some(String::from("idk")),
1236                    }),
1237                    created: String::from("2021-07-18T15:00:38+03:00"),
1238                },
1239                GiteaCommit {
1240                    sha: String::from("kk34967147560e809658776d4901709139b4ad68"),
1241                    author: None,
1242                    created: String::new(),
1243                },
1244                GiteaCommit {
1245                    sha: String::new(),
1246                    author: None,
1247                    created: String::new(),
1248                },
1249            ]
1250            .into_iter()
1251            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
1252            .collect(),
1253            vec![
1254                GiteaPullRequest {
1255                    title: Some(String::from("1")),
1256                    number: 42,
1257                    merge_commit_sha: Some(String::from(
1258                        "1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
1259                    )),
1260                    labels: vec![PullRequestLabel {
1261                        name: String::from("rust"),
1262                    }],
1263                },
1264                GiteaPullRequest {
1265                    title: Some(String::from("2")),
1266                    number: 66,
1267                    merge_commit_sha: Some(String::from(
1268                        "21f6aa587fcb772de13f2fde0e92697c51f84162",
1269                    )),
1270                    labels: vec![PullRequestLabel {
1271                        name: String::from("rust"),
1272                    }],
1273                },
1274                GiteaPullRequest {
1275                    title: Some(String::from("3")),
1276                    number: 53,
1277                    merge_commit_sha: Some(String::from(
1278                        "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
1279                    )),
1280                    labels: vec![PullRequestLabel {
1281                        name: String::from("deps"),
1282                    }],
1283                },
1284                GiteaPullRequest {
1285                    title: Some(String::from("4")),
1286                    number: 1000,
1287                    merge_commit_sha: Some(String::from(
1288                        "4d3ffe4753b923f4d7807c490e650e6624a12074",
1289                    )),
1290                    labels: vec![PullRequestLabel {
1291                        name: String::from("deps"),
1292                    }],
1293                },
1294                GiteaPullRequest {
1295                    title: Some(String::from("5")),
1296                    number: 999999,
1297                    merge_commit_sha: Some(String::from(
1298                        "5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
1299                    )),
1300                    labels: vec![PullRequestLabel {
1301                        name: String::from("github"),
1302                    }],
1303                },
1304            ]
1305            .into_iter()
1306            .map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
1307            .collect(),
1308        )?;
1309        #[allow(deprecated)]
1310        let expected_commits = vec![
1311            Commit {
1312                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1313                message: String::from("add github integration"),
1314                gitea: RemoteContributor {
1315                    username: Some(String::from("orhun")),
1316                    pr_title: Some(String::from("1")),
1317                    pr_number: Some(42),
1318                    pr_labels: vec![String::from("rust")],
1319                    is_first_time: false,
1320                },
1321                remote: Some(RemoteContributor {
1322                    username: Some(String::from("orhun")),
1323                    pr_title: Some(String::from("1")),
1324                    pr_number: Some(42),
1325                    pr_labels: vec![String::from("rust")],
1326                    is_first_time: false,
1327                }),
1328                ..Default::default()
1329            },
1330            Commit {
1331                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1332                message: String::from("fix github integration"),
1333                gitea: RemoteContributor {
1334                    username: Some(String::from("orhun")),
1335                    pr_title: Some(String::from("2")),
1336                    pr_number: Some(66),
1337                    pr_labels: vec![String::from("rust")],
1338                    is_first_time: false,
1339                },
1340                remote: Some(RemoteContributor {
1341                    username: Some(String::from("orhun")),
1342                    pr_title: Some(String::from("2")),
1343                    pr_number: Some(66),
1344                    pr_labels: vec![String::from("rust")],
1345                    is_first_time: false,
1346                }),
1347                ..Default::default()
1348            },
1349            Commit {
1350                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1351                message: String::from("update metadata"),
1352                gitea: RemoteContributor {
1353                    username: Some(String::from("nuhro")),
1354                    pr_title: Some(String::from("3")),
1355                    pr_number: Some(53),
1356                    pr_labels: vec![String::from("deps")],
1357                    is_first_time: false,
1358                },
1359                remote: Some(RemoteContributor {
1360                    username: Some(String::from("nuhro")),
1361                    pr_title: Some(String::from("3")),
1362                    pr_number: Some(53),
1363                    pr_labels: vec![String::from("deps")],
1364                    is_first_time: false,
1365                }),
1366                ..Default::default()
1367            },
1368            Commit {
1369                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1370                message: String::from("do some stuff"),
1371                gitea: RemoteContributor {
1372                    username: Some(String::from("awesome_contributor")),
1373                    pr_title: Some(String::from("4")),
1374                    pr_number: Some(1000),
1375                    pr_labels: vec![String::from("deps")],
1376                    is_first_time: false,
1377                },
1378                remote: Some(RemoteContributor {
1379                    username: Some(String::from("awesome_contributor")),
1380                    pr_title: Some(String::from("4")),
1381                    pr_number: Some(1000),
1382                    pr_labels: vec![String::from("deps")],
1383                    is_first_time: false,
1384                }),
1385                ..Default::default()
1386            },
1387            Commit {
1388                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1389                message: String::from("alright"),
1390                gitea: RemoteContributor {
1391                    username: Some(String::from("orhun")),
1392                    pr_title: Some(String::from("5")),
1393                    pr_number: Some(999999),
1394                    pr_labels: vec![String::from("github")],
1395                    is_first_time: false,
1396                },
1397                remote: Some(RemoteContributor {
1398                    username: Some(String::from("orhun")),
1399                    pr_title: Some(String::from("5")),
1400                    pr_number: Some(999999),
1401                    pr_labels: vec![String::from("github")],
1402                    is_first_time: false,
1403                }),
1404                ..Default::default()
1405            },
1406            Commit {
1407                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1408                message: String::from("should be fine"),
1409                gitea: RemoteContributor {
1410                    username: Some(String::from("someone")),
1411                    pr_title: None,
1412                    pr_number: None,
1413                    pr_labels: vec![],
1414                    is_first_time: false,
1415                },
1416                remote: Some(RemoteContributor {
1417                    username: Some(String::from("someone")),
1418                    pr_title: None,
1419                    pr_number: None,
1420                    pr_labels: vec![],
1421                    is_first_time: false,
1422                }),
1423                ..Default::default()
1424            },
1425        ];
1426        assert_eq!(expected_commits, release.commits);
1427
1428        release
1429            .gitea
1430            .contributors
1431            .sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
1432
1433        let expected_metadata = RemoteReleaseMetadata {
1434            contributors: vec![
1435                RemoteContributor {
1436                    username: Some(String::from("someone")),
1437                    pr_title: None,
1438                    pr_number: None,
1439                    pr_labels: vec![],
1440                    is_first_time: true,
1441                },
1442                RemoteContributor {
1443                    username: Some(String::from("orhun")),
1444                    pr_title: Some(String::from("1")),
1445                    pr_number: Some(42),
1446                    pr_labels: vec![String::from("rust")],
1447                    is_first_time: true,
1448                },
1449                RemoteContributor {
1450                    username: Some(String::from("nuhro")),
1451                    pr_title: Some(String::from("3")),
1452                    pr_number: Some(53),
1453                    pr_labels: vec![String::from("deps")],
1454                    is_first_time: true,
1455                },
1456                RemoteContributor {
1457                    username: Some(String::from("awesome_contributor")),
1458                    pr_title: Some(String::from("4")),
1459                    pr_number: Some(1000),
1460                    pr_labels: vec![String::from("deps")],
1461                    is_first_time: true,
1462                },
1463            ],
1464        };
1465        assert_eq!(expected_metadata, release.gitea);
1466
1467        Ok(())
1468    }
1469
1470    #[cfg(feature = "bitbucket")]
1471    #[test]
1472    fn update_bitbucket_metadata() -> Result<()> {
1473        use crate::remote::bitbucket::{
1474            BitbucketCommit, BitbucketCommitAuthor, BitbucketPullRequest,
1475            BitbucketPullRequestMergeCommit,
1476        };
1477
1478        let mut release = Release {
1479            version: None,
1480            message: None,
1481            extra: None,
1482            commits: vec![
1483                Commit::from(String::from(
1484                    "1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add bitbucket integration",
1485                )),
1486                Commit::from(String::from(
1487                    "21f6aa587fcb772de13f2fde0e92697c51f84162 fix bitbucket integration",
1488                )),
1489                Commit::from(String::from(
1490                    "35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
1491                )),
1492                Commit::from(String::from(
1493                    "4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
1494                )),
1495                Commit::from(String::from(
1496                    "5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
1497                )),
1498                Commit::from(String::from(
1499                    "6c34967147560ea09658776d4901709139b4ad66 should be fine",
1500                )),
1501            ],
1502            commit_range: None,
1503            commit_id: None,
1504            timestamp: None,
1505            previous: Some(Box::new(Release {
1506                version: Some(String::from("1.0.0")),
1507                ..Default::default()
1508            })),
1509            repository: Some(String::from("/root/repo")),
1510            submodule_commits: HashMap::new(),
1511            statistics: None,
1512            #[cfg(feature = "github")]
1513            github: RemoteReleaseMetadata {
1514                contributors: vec![],
1515            },
1516            #[cfg(feature = "gitlab")]
1517            gitlab: RemoteReleaseMetadata {
1518                contributors: vec![],
1519            },
1520            #[cfg(feature = "gitea")]
1521            gitea: RemoteReleaseMetadata {
1522                contributors: vec![],
1523            },
1524            #[cfg(feature = "bitbucket")]
1525            bitbucket: RemoteReleaseMetadata {
1526                contributors: vec![],
1527            },
1528        };
1529        release.update_bitbucket_metadata(
1530            vec![
1531                BitbucketCommit {
1532                    hash: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1533                    author: Some(BitbucketCommitAuthor {
1534                        login: Some(String::from("orhun")),
1535                    }),
1536                    date: String::from("2021-07-18T15:14:39+03:00"),
1537                },
1538                BitbucketCommit {
1539                    hash: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1540                    author: Some(BitbucketCommitAuthor {
1541                        login: Some(String::from("orhun")),
1542                    }),
1543                    date: String::from("2021-07-18T15:12:19+03:00"),
1544                },
1545                BitbucketCommit {
1546                    hash: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1547                    author: Some(BitbucketCommitAuthor {
1548                        login: Some(String::from("nuhro")),
1549                    }),
1550                    date: String::from("2021-07-18T15:07:23+03:00"),
1551                },
1552                BitbucketCommit {
1553                    hash: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1554                    author: Some(BitbucketCommitAuthor {
1555                        login: Some(String::from("awesome_contributor")),
1556                    }),
1557                    date: String::from("2021-07-18T15:05:10+03:00"),
1558                },
1559                BitbucketCommit {
1560                    hash: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1561                    author: Some(BitbucketCommitAuthor {
1562                        login: Some(String::from("orhun")),
1563                    }),
1564                    date: String::from("2021-07-18T15:03:30+03:00"),
1565                },
1566                BitbucketCommit {
1567                    hash: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1568                    author: Some(BitbucketCommitAuthor {
1569                        login: Some(String::from("someone")),
1570                    }),
1571                    date: String::from("2021-07-18T15:00:38+03:00"),
1572                },
1573                BitbucketCommit {
1574                    hash: String::from("0c34967147560e809658776d4901709139b4ad68"),
1575                    author: Some(BitbucketCommitAuthor {
1576                        login: Some(String::from("idk")),
1577                    }),
1578                    date: String::from("2021-07-18T15:00:01+03:00"),
1579                },
1580                BitbucketCommit {
1581                    hash: String::from("kk34967147560e809658776d4901709139b4ad68"),
1582                    author: Some(BitbucketCommitAuthor {
1583                        login: Some(String::from("orhun")),
1584                    }),
1585                    date: String::from("2021-07-14T21:25:24+03:00"),
1586                },
1587            ]
1588            .into_iter()
1589            .map(|v| Box::new(v) as Box<dyn RemoteCommit>)
1590            .collect(),
1591            vec![Box::new(BitbucketPullRequest {
1592                id: 1,
1593                title: Some(String::from("1")),
1594                author: BitbucketCommitAuthor {
1595                    login: Some(String::from("42")),
1596                },
1597                merge_commit: BitbucketPullRequestMergeCommit {
1598                    // Bitbucket merge commits returned in short format
1599                    hash: String::from("1d244937ee6c"),
1600                },
1601            })],
1602        )?;
1603        #[allow(deprecated)]
1604        let expected_commits = vec![
1605            Commit {
1606                id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1607                message: String::from("add bitbucket integration"),
1608                bitbucket: RemoteContributor {
1609                    username: Some(String::from("orhun")),
1610                    pr_title: Some(String::from("1")),
1611                    pr_number: Some(1),
1612                    pr_labels: vec![],
1613                    is_first_time: false,
1614                },
1615                remote: Some(RemoteContributor {
1616                    username: Some(String::from("orhun")),
1617                    pr_title: Some(String::from("1")),
1618                    pr_number: Some(1),
1619                    pr_labels: vec![],
1620                    is_first_time: false,
1621                }),
1622                ..Default::default()
1623            },
1624            Commit {
1625                id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1626                message: String::from("fix bitbucket integration"),
1627                bitbucket: RemoteContributor {
1628                    username: Some(String::from("orhun")),
1629                    pr_title: None,
1630                    pr_number: None,
1631                    pr_labels: vec![],
1632                    is_first_time: false,
1633                },
1634                remote: Some(RemoteContributor {
1635                    username: Some(String::from("orhun")),
1636                    pr_title: None,
1637                    pr_number: None,
1638                    pr_labels: vec![],
1639                    is_first_time: false,
1640                }),
1641                ..Default::default()
1642            },
1643            Commit {
1644                id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1645                message: String::from("update metadata"),
1646                bitbucket: RemoteContributor {
1647                    username: Some(String::from("nuhro")),
1648                    pr_title: None,
1649                    pr_number: None,
1650                    pr_labels: vec![],
1651                    is_first_time: false,
1652                },
1653                remote: Some(RemoteContributor {
1654                    username: Some(String::from("nuhro")),
1655                    pr_title: None,
1656                    pr_number: None,
1657                    pr_labels: vec![],
1658                    is_first_time: false,
1659                }),
1660                ..Default::default()
1661            },
1662            Commit {
1663                id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1664                message: String::from("do some stuff"),
1665                bitbucket: RemoteContributor {
1666                    username: Some(String::from("awesome_contributor")),
1667                    pr_title: None,
1668                    pr_number: None,
1669                    pr_labels: vec![],
1670                    is_first_time: false,
1671                },
1672                remote: Some(RemoteContributor {
1673                    username: Some(String::from("awesome_contributor")),
1674                    pr_title: None,
1675                    pr_number: None,
1676                    pr_labels: vec![],
1677                    is_first_time: false,
1678                }),
1679                ..Default::default()
1680            },
1681            Commit {
1682                id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1683                message: String::from("alright"),
1684                bitbucket: RemoteContributor {
1685                    username: Some(String::from("orhun")),
1686                    pr_title: None,
1687                    pr_number: None,
1688                    pr_labels: vec![],
1689                    is_first_time: false,
1690                },
1691                remote: Some(RemoteContributor {
1692                    username: Some(String::from("orhun")),
1693                    pr_title: None,
1694                    pr_number: None,
1695                    pr_labels: vec![],
1696                    is_first_time: false,
1697                }),
1698                ..Default::default()
1699            },
1700            Commit {
1701                id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1702                message: String::from("should be fine"),
1703                bitbucket: RemoteContributor {
1704                    username: Some(String::from("someone")),
1705                    pr_title: None,
1706                    pr_number: None,
1707                    pr_labels: vec![],
1708                    is_first_time: false,
1709                },
1710                remote: Some(RemoteContributor {
1711                    username: Some(String::from("someone")),
1712                    pr_title: None,
1713                    pr_number: None,
1714                    pr_labels: vec![],
1715                    is_first_time: false,
1716                }),
1717                ..Default::default()
1718            },
1719        ];
1720        assert_eq!(expected_commits, release.commits);
1721
1722        release
1723            .bitbucket
1724            .contributors
1725            .sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
1726
1727        let expected_metadata = RemoteReleaseMetadata {
1728            contributors: vec![
1729                RemoteContributor {
1730                    username: Some(String::from("nuhro")),
1731                    pr_title: None,
1732                    pr_number: None,
1733                    pr_labels: vec![],
1734                    is_first_time: true,
1735                },
1736                RemoteContributor {
1737                    username: Some(String::from("awesome_contributor")),
1738                    pr_title: None,
1739                    pr_number: None,
1740                    pr_labels: vec![],
1741                    is_first_time: true,
1742                },
1743                RemoteContributor {
1744                    username: Some(String::from("someone")),
1745                    pr_title: None,
1746                    pr_number: None,
1747                    pr_labels: vec![],
1748                    is_first_time: true,
1749                },
1750                RemoteContributor {
1751                    username: Some(String::from("orhun")),
1752                    pr_title: Some(String::from("1")),
1753                    pr_number: Some(1),
1754                    pr_labels: vec![],
1755                    is_first_time: false,
1756                },
1757            ],
1758        };
1759        assert_eq!(expected_metadata, release.bitbucket);
1760
1761        Ok(())
1762    }
1763}