git_cliff_core/
release.rs

1use std::collections::HashMap;
2
3use crate::commit::commits_to_conventional_commits;
4use crate::error::Result;
5use crate::{
6	commit::{
7		Commit,
8		Range,
9	},
10	config::Bump,
11	config::BumpType,
12};
13#[cfg(feature = "remote")]
14use crate::{
15	contributor::RemoteContributor,
16	remote::{
17		RemoteCommit,
18		RemotePullRequest,
19		RemoteReleaseMetadata,
20	},
21};
22
23use next_version::{
24	NextVersion,
25	VersionUpdater,
26};
27use semver::Version;
28use serde::{
29	Deserialize,
30	Serialize,
31};
32use serde_json::value::Value;
33
34/// Representation of a release.
35#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(rename_all(serialize = "camelCase"))]
37pub struct Release<'a> {
38	/// Release version, git tag.
39	pub version:           Option<String>,
40	/// git tag's message.
41	pub message:           Option<String>,
42	/// Commits made for the release.
43	#[serde(deserialize_with = "commits_to_conventional_commits")]
44	pub commits:           Vec<Commit<'a>>,
45	/// Commit ID of the tag.
46	#[serde(rename = "commit_id")]
47	pub commit_id:         Option<String>,
48	/// Timestamp of the release in seconds, from epoch.
49	pub timestamp:         i64,
50	/// Previous release.
51	pub previous:          Option<Box<Release<'a>>>,
52	/// Repository path.
53	pub repository:        Option<String>,
54	/// Commit range.
55	#[serde(rename = "commit_range")]
56	pub commit_range:      Option<Range>,
57	/// Submodule commits.
58	///
59	/// Maps submodule path to a list of commits.
60	#[serde(rename = "submodule_commits")]
61	pub submodule_commits: HashMap<String, Vec<Commit<'a>>>,
62	/// Arbitrary data to be used with the `--from-context` CLI option.
63	pub extra:             Option<Value>,
64	/// Contributors.
65	#[cfg(feature = "github")]
66	pub github:            RemoteReleaseMetadata,
67	/// Contributors.
68	#[cfg(feature = "gitlab")]
69	pub gitlab:            RemoteReleaseMetadata,
70	/// Contributors.
71	#[cfg(feature = "gitea")]
72	pub gitea:             RemoteReleaseMetadata,
73	/// Contributors.
74	#[cfg(feature = "bitbucket")]
75	pub bitbucket:         RemoteReleaseMetadata,
76}
77
78#[cfg(feature = "github")]
79crate::update_release_metadata!(github, update_github_metadata);
80
81#[cfg(feature = "gitlab")]
82crate::update_release_metadata!(gitlab, update_gitlab_metadata);
83
84#[cfg(feature = "gitea")]
85crate::update_release_metadata!(gitea, update_gitea_metadata);
86
87#[cfg(feature = "bitbucket")]
88crate::update_release_metadata!(bitbucket, update_bitbucket_metadata);
89
90impl Release<'_> {
91	/// Calculates the next version based on the commits.
92	///
93	/// It uses the default bump version configuration to calculate the next
94	/// version.
95	pub fn calculate_next_version(&self) -> Result<String> {
96		self.calculate_next_version_with_config(&Bump::default())
97	}
98
99	/// Calculates the next version based on the commits.
100	///
101	/// It uses the given bump version configuration to calculate the next
102	/// version.
103	pub(super) fn calculate_next_version_with_config(
104		&self,
105		config: &Bump,
106	) -> Result<String> {
107		match self
108			.previous
109			.as_ref()
110			.and_then(|release| release.version.clone())
111		{
112			Some(version) => {
113				let mut semver = Version::parse(&version);
114				let mut prefix = None;
115				if semver.is_err() && version.split('.').count() >= 2 {
116					let mut found_numeric = false;
117					for (i, c) in version.char_indices() {
118						if c.is_numeric() && !found_numeric {
119							found_numeric = true;
120							let version_prefix = version[..i].to_string();
121							let remaining = version[i..].to_string();
122							let version = Version::parse(&remaining);
123							if version.is_ok() {
124								semver = version;
125								prefix = Some(version_prefix);
126								break;
127							}
128						} else if !c.is_numeric() && found_numeric {
129							found_numeric = false;
130						}
131					}
132				}
133				let mut next_version = VersionUpdater::new()
134					.with_features_always_increment_minor(
135						config.features_always_bump_minor.unwrap_or(true),
136					)
137					.with_breaking_always_increment_major(
138						config.breaking_always_bump_major.unwrap_or(true),
139					);
140				if let Some(custom_major_increment_regex) =
141					&config.custom_major_increment_regex
142				{
143					next_version = next_version.with_custom_major_increment_regex(
144						custom_major_increment_regex,
145					)?;
146				}
147				if let Some(custom_minor_increment_regex) =
148					&config.custom_minor_increment_regex
149				{
150					next_version = next_version.with_custom_minor_increment_regex(
151						custom_minor_increment_regex,
152					)?;
153				}
154				let next_version = if let Some(bump_type) = &config.bump_type {
155					match bump_type {
156						BumpType::Major => semver?.increment_major().to_string(),
157						BumpType::Minor => semver?.increment_minor().to_string(),
158						BumpType::Patch => semver?.increment_patch().to_string(),
159					}
160				} else {
161					next_version
162						.increment(
163							&semver?,
164							self.commits
165								.iter()
166								.map(|commit| commit.message.trim_end().to_string())
167								.collect::<Vec<String>>(),
168						)
169						.to_string()
170				};
171				if let Some(prefix) = prefix {
172					Ok(format!("{prefix}{next_version}"))
173				} else {
174					Ok(next_version)
175				}
176			}
177			None => Ok(config.get_initial_tag()),
178		}
179	}
180}
181
182/// Representation of a list of releases.
183#[derive(Serialize)]
184pub struct Releases<'a> {
185	/// Releases.
186	pub releases: &'a Vec<Release<'a>>,
187}
188
189impl Releases<'_> {
190	/// Returns the list of releases as JSON.
191	pub fn as_json(&self) -> Result<String> {
192		Ok(serde_json::to_string(self.releases)?)
193	}
194}
195
196#[cfg(test)]
197mod test {
198	use super::*;
199	use pretty_assertions::assert_eq;
200	#[test]
201	fn bump_version() -> Result<()> {
202		fn build_release<'a>(version: &str, commits: &'a [&str]) -> Release<'a> {
203			Release {
204				version: None,
205				message: None,
206				extra: None,
207				commits: commits
208					.iter()
209					.map(|v| Commit::from(v.to_string()))
210					.collect(),
211				commit_range: None,
212				commit_id: None,
213				timestamp: 0,
214				previous: Some(Box::new(Release {
215					version: Some(String::from(version)),
216					..Default::default()
217				})),
218				repository: Some(String::from("/root/repo")),
219				submodule_commits: HashMap::new(),
220				#[cfg(feature = "github")]
221				github: crate::remote::RemoteReleaseMetadata {
222					contributors: vec![],
223				},
224				#[cfg(feature = "gitlab")]
225				gitlab: crate::remote::RemoteReleaseMetadata {
226					contributors: vec![],
227				},
228				#[cfg(feature = "gitea")]
229				gitea: crate::remote::RemoteReleaseMetadata {
230					contributors: vec![],
231				},
232				#[cfg(feature = "bitbucket")]
233				bitbucket: crate::remote::RemoteReleaseMetadata {
234					contributors: vec![],
235				},
236			}
237		}
238
239		let test_shared = [
240			("1.0.0", "1.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
241			("1.0.0", "1.0.1", vec!["fix: add xyz", "fix: aaaaaa"]),
242			("1.0.0", "2.0.0", vec!["feat!: add xyz", "feat: zzz"]),
243			("1.0.0", "2.0.0", vec!["feat!: add xyz\n", "feat: zzz\n"]),
244			("2.0.0", "2.0.1", vec!["fix: something"]),
245			("foo/1.0.0", "foo/1.1.0", vec![
246				"feat: add xyz",
247				"fix: fix xyz",
248			]),
249			("bar/1.0.0", "bar/2.0.0", vec![
250				"fix: add xyz",
251				"fix!: aaaaaa",
252			]),
253			("zzz-123/test/1.0.0", "zzz-123/test/1.0.1", vec![
254				"fix: aaaaaa",
255			]),
256			("v100.0.0", "v101.0.0", vec!["feat!: something"]),
257			("v1.0.0-alpha.1", "v1.0.0-alpha.2", vec!["fix: minor"]),
258			("testing/v1.0.0-beta.1", "testing/v1.0.0-beta.2", vec![
259				"feat: nice",
260			]),
261			("tauri-v1.5.4", "tauri-v1.6.0", vec!["feat: something"]),
262			(
263				"rocket/rocket-v4.0.0-rc.1",
264				"rocket/rocket-v4.0.0-rc.2",
265				vec!["chore!: wow"],
266			),
267			(
268				"aaa#/@#$@9384!#%^#@#@!#!239432413-idk-9999.2200.5932-alpha.419",
269				"aaa#/@#$@9384!#%^#@#@!#!239432413-idk-9999.2200.5932-alpha.420",
270				vec!["feat: damn this is working"],
271			),
272		];
273
274		for (version, expected_version, commits) in test_shared.iter().chain(
275			[
276				("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
277				("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
278				("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
279				("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
280				("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]),
281				("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
282			]
283			.iter(),
284		) {
285			let release = build_release(version, commits);
286			let next_version = release.calculate_next_version()?;
287			assert_eq!(expected_version, &next_version);
288			let next_version =
289				release.calculate_next_version_with_config(&Bump::default())?;
290			assert_eq!(expected_version, &next_version);
291		}
292
293		for (version, expected_version, commits) in test_shared.iter().chain(
294			[
295				("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
296				("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]),
297				("0.0.1", "0.0.2", vec!["feat!: add xyz", "feat: zzz"]),
298				("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
299				("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]),
300				("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]),
301			]
302			.iter(),
303		) {
304			let release = build_release(version, commits);
305			let next_version =
306				release.calculate_next_version_with_config(&Bump {
307					features_always_bump_minor:   Some(false),
308					breaking_always_bump_major:   Some(false),
309					initial_tag:                  None,
310					custom_major_increment_regex: None,
311					custom_minor_increment_regex: None,
312					bump_type:                    None,
313				})?;
314			assert_eq!(expected_version, &next_version);
315		}
316
317		for (version, expected_version, commits) in test_shared.iter().chain(
318			[
319				("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
320				("0.0.1", "0.1.0", vec!["feat: add xyz", "fix: fix xyz"]),
321				("0.0.1", "0.1.0", vec!["feat!: add xyz", "feat: zzz"]),
322				("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
323				("0.1.0", "0.2.0", vec!["feat: add xyz", "fix: fix xyz"]),
324				("0.1.0", "0.2.0", vec!["feat!: add xyz", "feat: zzz"]),
325			]
326			.iter(),
327		) {
328			let release = build_release(version, commits);
329			let next_version =
330				release.calculate_next_version_with_config(&Bump {
331					features_always_bump_minor:   Some(true),
332					breaking_always_bump_major:   Some(false),
333					initial_tag:                  None,
334					custom_major_increment_regex: None,
335					custom_minor_increment_regex: None,
336					bump_type:                    None,
337				})?;
338			assert_eq!(expected_version, &next_version);
339		}
340
341		for (version, expected_version, commits) in test_shared.iter().chain(
342			[
343				("0.0.1", "0.0.2", vec!["fix: fix xyz"]),
344				("0.0.1", "0.0.2", vec!["feat: add xyz", "fix: fix xyz"]),
345				("0.0.1", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
346				("0.1.0", "0.1.1", vec!["fix: fix xyz"]),
347				("0.1.0", "0.1.1", vec!["feat: add xyz", "fix: fix xyz"]),
348				("0.1.0", "1.0.0", vec!["feat!: add xyz", "feat: zzz"]),
349			]
350			.iter(),
351		) {
352			let release = build_release(version, commits);
353			let next_version =
354				release.calculate_next_version_with_config(&Bump {
355					features_always_bump_minor:   Some(false),
356					breaking_always_bump_major:   Some(true),
357					initial_tag:                  None,
358					custom_major_increment_regex: None,
359					custom_minor_increment_regex: None,
360					bump_type:                    None,
361				})?;
362			assert_eq!(expected_version, &next_version);
363		}
364
365		let empty_release = Release {
366			previous: Some(Box::new(Release {
367				version: None,
368				..Default::default()
369			})),
370			..Default::default()
371		};
372		assert_eq!("0.1.0", empty_release.calculate_next_version()?);
373		for (features_always_bump_minor, breaking_always_bump_major) in
374			[(true, true), (true, false), (false, true), (false, false)]
375		{
376			assert_eq!(
377				"0.1.0",
378				empty_release.calculate_next_version_with_config(&Bump {
379					features_always_bump_minor:   Some(features_always_bump_minor),
380					breaking_always_bump_major:   Some(breaking_always_bump_major),
381					initial_tag:                  None,
382					custom_major_increment_regex: None,
383					custom_minor_increment_regex: None,
384					bump_type:                    None,
385				})?
386			);
387		}
388		Ok(())
389	}
390
391	#[cfg(feature = "github")]
392	#[test]
393	fn update_github_metadata() -> Result<()> {
394		use crate::remote::github::{
395			GitHubCommit,
396			GitHubCommitAuthor,
397			GitHubCommitDetails,
398			GitHubCommitDetailsAuthor,
399			GitHubPullRequest,
400			PullRequestLabel,
401		};
402
403		let mut release = Release {
404			version: None,
405			message: None,
406			extra: None,
407			commits: vec![
408				Commit::from(String::from(
409					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
410					 integration",
411				)),
412				Commit::from(String::from(
413					"21f6aa587fcb772de13f2fde0e92697c51f84162 fix github \
414					 integration",
415				)),
416				Commit::from(String::from(
417					"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
418				)),
419				Commit::from(String::from(
420					"4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
421				)),
422				Commit::from(String::from(
423					"5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
424				)),
425				Commit::from(String::from(
426					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
427				)),
428			],
429			commit_range: None,
430			commit_id: None,
431			timestamp: 0,
432			previous: Some(Box::new(Release {
433				version: Some(String::from("1.0.0")),
434				..Default::default()
435			})),
436			repository: Some(String::from("/root/repo")),
437			submodule_commits: HashMap::new(),
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::{
768			GitLabCommit,
769			GitLabMergeRequest,
770			GitLabUser,
771		};
772
773		let mut release = Release {
774			version: None,
775			message: None,
776			extra: None,
777			commits: vec![
778				Commit::from(String::from(
779					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
780					 integration",
781				)),
782				Commit::from(String::from(
783					"21f6aa587fcb772de13f2fde0e92697c51f84162 fix github \
784					 integration",
785				)),
786				Commit::from(String::from(
787					"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
788				)),
789				Commit::from(String::from(
790					"4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
791				)),
792				Commit::from(String::from(
793					"5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
794				)),
795				Commit::from(String::from(
796					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
797				)),
798			],
799			commit_range: None,
800			commit_id: None,
801			timestamp: 0,
802			previous: Some(Box::new(Release {
803				version: Some(String::from("1.0.0")),
804				..Default::default()
805			})),
806			repository: Some(String::from("/root/repo")),
807			submodule_commits: HashMap::new(),
808			#[cfg(feature = "github")]
809			github: RemoteReleaseMetadata {
810				contributors: vec![],
811			},
812			#[cfg(feature = "gitlab")]
813			gitlab: RemoteReleaseMetadata {
814				contributors: vec![],
815			},
816			#[cfg(feature = "gitea")]
817			gitea: RemoteReleaseMetadata {
818				contributors: vec![],
819			},
820			#[cfg(feature = "bitbucket")]
821			bitbucket: RemoteReleaseMetadata {
822				contributors: vec![],
823			},
824		};
825		release.update_gitlab_metadata(
826			vec![
827				GitLabCommit {
828					id:              String::from(
829						"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
830					),
831					author_name:     String::from("orhun"),
832					short_id:        String::from(""),
833					title:           String::from(""),
834					author_email:    String::from(""),
835					authored_date:   String::from(""),
836					committer_name:  String::from(""),
837					committer_email: String::from(""),
838					committed_date:  String::from(""),
839					created_at:      String::from(""),
840					message:         String::from(""),
841					parent_ids:      vec![],
842					web_url:         String::from(""),
843				},
844				GitLabCommit {
845					id:              String::from(
846						"21f6aa587fcb772de13f2fde0e92697c51f84162",
847					),
848					author_name:     String::from("orhun"),
849					short_id:        String::from(""),
850					title:           String::from(""),
851					author_email:    String::from(""),
852					authored_date:   String::from(""),
853					committer_name:  String::from(""),
854					committer_email: String::from(""),
855					committed_date:  String::from(""),
856					created_at:      String::from(""),
857					message:         String::from(""),
858					parent_ids:      vec![],
859					web_url:         String::from(""),
860				},
861				GitLabCommit {
862					id:              String::from(
863						"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
864					),
865					author_name:     String::from("nuhro"),
866					short_id:        String::from(""),
867					title:           String::from(""),
868					author_email:    String::from(""),
869					authored_date:   String::from(""),
870					committer_name:  String::from(""),
871					committer_email: String::from(""),
872					committed_date:  String::from(""),
873					created_at:      String::from(""),
874					message:         String::from(""),
875					parent_ids:      vec![],
876					web_url:         String::from(""),
877				},
878				GitLabCommit {
879					id:              String::from(
880						"4d3ffe4753b923f4d7807c490e650e6624a12074",
881					),
882					author_name:     String::from("awesome_contributor"),
883					short_id:        String::from(""),
884					title:           String::from(""),
885					author_email:    String::from(""),
886					authored_date:   String::from(""),
887					committer_name:  String::from(""),
888					committer_email: String::from(""),
889					committed_date:  String::from(""),
890					created_at:      String::from(""),
891					message:         String::from(""),
892					parent_ids:      vec![],
893					web_url:         String::from(""),
894				},
895				GitLabCommit {
896					id:              String::from(
897						"5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
898					),
899					author_name:     String::from("orhun"),
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(
914						"6c34967147560ea09658776d4901709139b4ad66",
915					),
916					author_name:     String::from("someone"),
917					short_id:        String::from(""),
918					title:           String::from(""),
919					author_email:    String::from(""),
920					authored_date:   String::from(""),
921					committer_name:  String::from(""),
922					committer_email: String::from(""),
923					committed_date:  String::from(""),
924					created_at:      String::from(""),
925					message:         String::from(""),
926					parent_ids:      vec![],
927					web_url:         String::from(""),
928				},
929				GitLabCommit {
930					id:              String::from(
931						"0c34967147560e809658776d4901709139b4ad68",
932					),
933					author_name:     String::from("idk"),
934					short_id:        String::from(""),
935					title:           String::from(""),
936					author_email:    String::from(""),
937					authored_date:   String::from(""),
938					committer_name:  String::from(""),
939					committer_email: String::from(""),
940					committed_date:  String::from(""),
941					created_at:      String::from(""),
942					message:         String::from(""),
943					parent_ids:      vec![],
944					web_url:         String::from(""),
945				},
946				GitLabCommit {
947					id:              String::from(
948						"kk34967147560e809658776d4901709139b4ad68",
949					),
950					author_name:     String::from("orhun"),
951					short_id:        String::from(""),
952					title:           String::from(""),
953					author_email:    String::from(""),
954					authored_date:   String::from(""),
955					committer_name:  String::from(""),
956					committer_email: String::from(""),
957					committed_date:  String::from(""),
958					created_at:      String::from(""),
959					message:         String::from(""),
960					parent_ids:      vec![],
961					web_url:         String::from(""),
962				},
963			]
964			.into_iter()
965			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
966			.collect(),
967			vec![Box::new(GitLabMergeRequest {
968				title:             String::from("1"),
969				merge_commit_sha:  Some(String::from(
970					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
971				)),
972				id:                1,
973				iid:               1,
974				project_id:        1,
975				description:       String::from(""),
976				state:             String::from(""),
977				created_at:        String::from(""),
978				author:            GitLabUser {
979					id:         1,
980					name:       String::from("42"),
981					username:   String::from("42"),
982					state:      String::from("42"),
983					avatar_url: None,
984					web_url:    String::from("42"),
985				},
986				sha:               String::from(
987					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
988				),
989				web_url:           String::from(""),
990				squash_commit_sha: None,
991				labels:            vec![String::from("rust")],
992			})],
993		)?;
994		#[allow(deprecated)]
995		let expected_commits = vec![
996			Commit {
997				id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
998				message: String::from("add github integration"),
999				gitlab: RemoteContributor {
1000					username:      Some(String::from("orhun")),
1001					pr_title:      Some(String::from("1")),
1002					pr_number:     Some(1),
1003					pr_labels:     vec![String::from("rust")],
1004					is_first_time: false,
1005				},
1006				remote: Some(RemoteContributor {
1007					username:      Some(String::from("orhun")),
1008					pr_title:      Some(String::from("1")),
1009					pr_number:     Some(1),
1010					pr_labels:     vec![String::from("rust")],
1011					is_first_time: false,
1012				}),
1013				..Default::default()
1014			},
1015			Commit {
1016				id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1017				message: String::from("fix github integration"),
1018				gitlab: RemoteContributor {
1019					username:      Some(String::from("orhun")),
1020					pr_title:      None,
1021					pr_number:     None,
1022					pr_labels:     vec![],
1023					is_first_time: false,
1024				},
1025				remote: Some(RemoteContributor {
1026					username:      Some(String::from("orhun")),
1027					pr_title:      None,
1028					pr_number:     None,
1029					pr_labels:     vec![],
1030					is_first_time: false,
1031				}),
1032				..Default::default()
1033			},
1034			Commit {
1035				id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1036				message: String::from("update metadata"),
1037				gitlab: RemoteContributor {
1038					username:      Some(String::from("nuhro")),
1039					pr_title:      None,
1040					pr_number:     None,
1041					pr_labels:     vec![],
1042					is_first_time: false,
1043				},
1044				remote: Some(RemoteContributor {
1045					username:      Some(String::from("nuhro")),
1046					pr_title:      None,
1047					pr_number:     None,
1048					pr_labels:     vec![],
1049					is_first_time: false,
1050				}),
1051				..Default::default()
1052			},
1053			Commit {
1054				id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1055				message: String::from("do some stuff"),
1056				gitlab: RemoteContributor {
1057					username:      Some(String::from("awesome_contributor")),
1058					pr_title:      None,
1059					pr_number:     None,
1060					pr_labels:     vec![],
1061					is_first_time: false,
1062				},
1063				remote: Some(RemoteContributor {
1064					username:      Some(String::from("awesome_contributor")),
1065					pr_title:      None,
1066					pr_number:     None,
1067					pr_labels:     vec![],
1068					is_first_time: false,
1069				}),
1070				..Default::default()
1071			},
1072			Commit {
1073				id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1074				message: String::from("alright"),
1075				gitlab: RemoteContributor {
1076					username:      Some(String::from("orhun")),
1077					pr_title:      None,
1078					pr_number:     None,
1079					pr_labels:     vec![],
1080					is_first_time: false,
1081				},
1082				remote: Some(RemoteContributor {
1083					username:      Some(String::from("orhun")),
1084					pr_title:      None,
1085					pr_number:     None,
1086					pr_labels:     vec![],
1087					is_first_time: false,
1088				}),
1089				..Default::default()
1090			},
1091			Commit {
1092				id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1093				message: String::from("should be fine"),
1094				gitlab: RemoteContributor {
1095					username:      Some(String::from("someone")),
1096					pr_title:      None,
1097					pr_number:     None,
1098					pr_labels:     vec![],
1099					is_first_time: false,
1100				},
1101				remote: Some(RemoteContributor {
1102					username:      Some(String::from("someone")),
1103					pr_title:      None,
1104					pr_number:     None,
1105					pr_labels:     vec![],
1106					is_first_time: false,
1107				}),
1108				..Default::default()
1109			},
1110		];
1111		assert_eq!(expected_commits, release.commits);
1112
1113		release
1114			.github
1115			.contributors
1116			.sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
1117
1118		let expected_metadata = RemoteReleaseMetadata {
1119			contributors: vec![
1120				RemoteContributor {
1121					username:      Some(String::from("orhun")),
1122					pr_title:      Some(String::from("1")),
1123					pr_number:     Some(1),
1124					pr_labels:     vec![String::from("rust")],
1125					is_first_time: false,
1126				},
1127				RemoteContributor {
1128					username:      Some(String::from("nuhro")),
1129					pr_title:      None,
1130					pr_number:     None,
1131					pr_labels:     vec![],
1132					is_first_time: true,
1133				},
1134				RemoteContributor {
1135					username:      Some(String::from("awesome_contributor")),
1136					pr_title:      None,
1137					pr_number:     None,
1138					pr_labels:     vec![],
1139					is_first_time: true,
1140				},
1141				RemoteContributor {
1142					username:      Some(String::from("someone")),
1143					pr_title:      None,
1144					pr_number:     None,
1145					pr_labels:     vec![],
1146					is_first_time: true,
1147				},
1148			],
1149		};
1150		assert_eq!(expected_metadata, release.gitlab);
1151
1152		Ok(())
1153	}
1154
1155	#[cfg(feature = "gitea")]
1156	#[test]
1157	fn update_gitea_metadata() -> Result<()> {
1158		use crate::remote::gitea::{
1159			GiteaCommit,
1160			GiteaCommitAuthor,
1161			GiteaPullRequest,
1162			PullRequestLabel,
1163		};
1164
1165		let mut release = Release {
1166			version: None,
1167			message: None,
1168			extra: None,
1169			commits: vec![
1170				Commit::from(String::from(
1171					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add github \
1172					 integration",
1173				)),
1174				Commit::from(String::from(
1175					"21f6aa587fcb772de13f2fde0e92697c51f84162 fix github \
1176					 integration",
1177				)),
1178				Commit::from(String::from(
1179					"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
1180				)),
1181				Commit::from(String::from(
1182					"4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
1183				)),
1184				Commit::from(String::from(
1185					"5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
1186				)),
1187				Commit::from(String::from(
1188					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
1189				)),
1190			],
1191			commit_range: None,
1192			commit_id: None,
1193			timestamp: 0,
1194			previous: Some(Box::new(Release {
1195				version: Some(String::from("1.0.0")),
1196				..Default::default()
1197			})),
1198			repository: Some(String::from("/root/repo")),
1199			submodule_commits: HashMap::new(),
1200			#[cfg(feature = "github")]
1201			github: RemoteReleaseMetadata {
1202				contributors: vec![],
1203			},
1204			#[cfg(feature = "gitlab")]
1205			gitlab: RemoteReleaseMetadata {
1206				contributors: vec![],
1207			},
1208			#[cfg(feature = "gitea")]
1209			gitea: RemoteReleaseMetadata {
1210				contributors: vec![],
1211			},
1212			#[cfg(feature = "bitbucket")]
1213			bitbucket: RemoteReleaseMetadata {
1214				contributors: vec![],
1215			},
1216		};
1217		release.update_gitea_metadata(
1218			vec![
1219				GiteaCommit {
1220					sha:     String::from(
1221						"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
1222					),
1223					author:  Some(GiteaCommitAuthor {
1224						login: Some(String::from("orhun")),
1225					}),
1226					created: String::from("2021-07-18T15:14:39+03:00"),
1227				},
1228				GiteaCommit {
1229					sha:     String::from(
1230						"21f6aa587fcb772de13f2fde0e92697c51f84162",
1231					),
1232					author:  Some(GiteaCommitAuthor {
1233						login: Some(String::from("orhun")),
1234					}),
1235					created: String::from("2021-07-18T15:12:19+03:00"),
1236				},
1237				GiteaCommit {
1238					sha:     String::from(
1239						"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
1240					),
1241					author:  Some(GiteaCommitAuthor {
1242						login: Some(String::from("nuhro")),
1243					}),
1244					created: String::from("2021-07-18T15:07:23+03:00"),
1245				},
1246				GiteaCommit {
1247					sha:     String::from(
1248						"4d3ffe4753b923f4d7807c490e650e6624a12074",
1249					),
1250					author:  Some(GiteaCommitAuthor {
1251						login: Some(String::from("awesome_contributor")),
1252					}),
1253					created: String::from("2021-07-18T15:05:10+03:00"),
1254				},
1255				GiteaCommit {
1256					sha:     String::from(
1257						"5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
1258					),
1259					author:  Some(GiteaCommitAuthor {
1260						login: Some(String::from("orhun")),
1261					}),
1262					created: String::from("2021-07-18T15:03:30+03:00"),
1263				},
1264				GiteaCommit {
1265					sha:     String::from(
1266						"6c34967147560ea09658776d4901709139b4ad66",
1267					),
1268					author:  Some(GiteaCommitAuthor {
1269						login: Some(String::from("someone")),
1270					}),
1271					created: String::from("2021-07-18T15:00:38+03:00"),
1272				},
1273				GiteaCommit {
1274					sha:     String::from(
1275						"0c34967147560e809658776d4901709139b4ad68",
1276					),
1277					author:  Some(GiteaCommitAuthor {
1278						login: Some(String::from("idk")),
1279					}),
1280					created: String::from("2021-07-18T15:00:38+03:00"),
1281				},
1282				GiteaCommit {
1283					sha:     String::from(
1284						"kk34967147560e809658776d4901709139b4ad68",
1285					),
1286					author:  None,
1287					created: String::new(),
1288				},
1289				GiteaCommit {
1290					sha:     String::new(),
1291					author:  None,
1292					created: String::new(),
1293				},
1294			]
1295			.into_iter()
1296			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
1297			.collect(),
1298			vec![
1299				GiteaPullRequest {
1300					title:            Some(String::from("1")),
1301					number:           42,
1302					merge_commit_sha: Some(String::from(
1303						"1d244937ee6ceb8e0314a4a201ba93a7a61f2071",
1304					)),
1305					labels:           vec![PullRequestLabel {
1306						name: String::from("rust"),
1307					}],
1308				},
1309				GiteaPullRequest {
1310					title:            Some(String::from("2")),
1311					number:           66,
1312					merge_commit_sha: Some(String::from(
1313						"21f6aa587fcb772de13f2fde0e92697c51f84162",
1314					)),
1315					labels:           vec![PullRequestLabel {
1316						name: String::from("rust"),
1317					}],
1318				},
1319				GiteaPullRequest {
1320					title:            Some(String::from("3")),
1321					number:           53,
1322					merge_commit_sha: Some(String::from(
1323						"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973",
1324					)),
1325					labels:           vec![PullRequestLabel {
1326						name: String::from("deps"),
1327					}],
1328				},
1329				GiteaPullRequest {
1330					title:            Some(String::from("4")),
1331					number:           1000,
1332					merge_commit_sha: Some(String::from(
1333						"4d3ffe4753b923f4d7807c490e650e6624a12074",
1334					)),
1335					labels:           vec![PullRequestLabel {
1336						name: String::from("deps"),
1337					}],
1338				},
1339				GiteaPullRequest {
1340					title:            Some(String::from("5")),
1341					number:           999999,
1342					merge_commit_sha: Some(String::from(
1343						"5a55e92e5a62dc5bf9872ffb2566959fad98bd05",
1344					)),
1345					labels:           vec![PullRequestLabel {
1346						name: String::from("github"),
1347					}],
1348				},
1349			]
1350			.into_iter()
1351			.map(|v| Box::new(v) as Box<dyn RemotePullRequest>)
1352			.collect(),
1353		)?;
1354		#[allow(deprecated)]
1355		let expected_commits = vec![
1356			Commit {
1357				id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1358				message: String::from("add github integration"),
1359				gitea: RemoteContributor {
1360					username:      Some(String::from("orhun")),
1361					pr_title:      Some(String::from("1")),
1362					pr_number:     Some(42),
1363					pr_labels:     vec![String::from("rust")],
1364					is_first_time: false,
1365				},
1366				remote: Some(RemoteContributor {
1367					username:      Some(String::from("orhun")),
1368					pr_title:      Some(String::from("1")),
1369					pr_number:     Some(42),
1370					pr_labels:     vec![String::from("rust")],
1371					is_first_time: false,
1372				}),
1373				..Default::default()
1374			},
1375			Commit {
1376				id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1377				message: String::from("fix github integration"),
1378				gitea: RemoteContributor {
1379					username:      Some(String::from("orhun")),
1380					pr_title:      Some(String::from("2")),
1381					pr_number:     Some(66),
1382					pr_labels:     vec![String::from("rust")],
1383					is_first_time: false,
1384				},
1385				remote: Some(RemoteContributor {
1386					username:      Some(String::from("orhun")),
1387					pr_title:      Some(String::from("2")),
1388					pr_number:     Some(66),
1389					pr_labels:     vec![String::from("rust")],
1390					is_first_time: false,
1391				}),
1392				..Default::default()
1393			},
1394			Commit {
1395				id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1396				message: String::from("update metadata"),
1397				gitea: RemoteContributor {
1398					username:      Some(String::from("nuhro")),
1399					pr_title:      Some(String::from("3")),
1400					pr_number:     Some(53),
1401					pr_labels:     vec![String::from("deps")],
1402					is_first_time: false,
1403				},
1404				remote: Some(RemoteContributor {
1405					username:      Some(String::from("nuhro")),
1406					pr_title:      Some(String::from("3")),
1407					pr_number:     Some(53),
1408					pr_labels:     vec![String::from("deps")],
1409					is_first_time: false,
1410				}),
1411				..Default::default()
1412			},
1413			Commit {
1414				id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1415				message: String::from("do some stuff"),
1416				gitea: RemoteContributor {
1417					username:      Some(String::from("awesome_contributor")),
1418					pr_title:      Some(String::from("4")),
1419					pr_number:     Some(1000),
1420					pr_labels:     vec![String::from("deps")],
1421					is_first_time: false,
1422				},
1423				remote: Some(RemoteContributor {
1424					username:      Some(String::from("awesome_contributor")),
1425					pr_title:      Some(String::from("4")),
1426					pr_number:     Some(1000),
1427					pr_labels:     vec![String::from("deps")],
1428					is_first_time: false,
1429				}),
1430				..Default::default()
1431			},
1432			Commit {
1433				id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1434				message: String::from("alright"),
1435				gitea: RemoteContributor {
1436					username:      Some(String::from("orhun")),
1437					pr_title:      Some(String::from("5")),
1438					pr_number:     Some(999999),
1439					pr_labels:     vec![String::from("github")],
1440					is_first_time: false,
1441				},
1442				remote: Some(RemoteContributor {
1443					username:      Some(String::from("orhun")),
1444					pr_title:      Some(String::from("5")),
1445					pr_number:     Some(999999),
1446					pr_labels:     vec![String::from("github")],
1447					is_first_time: false,
1448				}),
1449				..Default::default()
1450			},
1451			Commit {
1452				id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1453				message: String::from("should be fine"),
1454				gitea: RemoteContributor {
1455					username:      Some(String::from("someone")),
1456					pr_title:      None,
1457					pr_number:     None,
1458					pr_labels:     vec![],
1459					is_first_time: false,
1460				},
1461				remote: Some(RemoteContributor {
1462					username:      Some(String::from("someone")),
1463					pr_title:      None,
1464					pr_number:     None,
1465					pr_labels:     vec![],
1466					is_first_time: false,
1467				}),
1468				..Default::default()
1469			},
1470		];
1471		assert_eq!(expected_commits, release.commits);
1472
1473		release
1474			.gitea
1475			.contributors
1476			.sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
1477
1478		let expected_metadata = RemoteReleaseMetadata {
1479			contributors: vec![
1480				RemoteContributor {
1481					username:      Some(String::from("someone")),
1482					pr_title:      None,
1483					pr_number:     None,
1484					pr_labels:     vec![],
1485					is_first_time: true,
1486				},
1487				RemoteContributor {
1488					username:      Some(String::from("orhun")),
1489					pr_title:      Some(String::from("1")),
1490					pr_number:     Some(42),
1491					pr_labels:     vec![String::from("rust")],
1492					is_first_time: true,
1493				},
1494				RemoteContributor {
1495					username:      Some(String::from("nuhro")),
1496					pr_title:      Some(String::from("3")),
1497					pr_number:     Some(53),
1498					pr_labels:     vec![String::from("deps")],
1499					is_first_time: true,
1500				},
1501				RemoteContributor {
1502					username:      Some(String::from("awesome_contributor")),
1503					pr_title:      Some(String::from("4")),
1504					pr_number:     Some(1000),
1505					pr_labels:     vec![String::from("deps")],
1506					is_first_time: true,
1507				},
1508			],
1509		};
1510		assert_eq!(expected_metadata, release.gitea);
1511
1512		Ok(())
1513	}
1514
1515	#[cfg(feature = "bitbucket")]
1516	#[test]
1517	fn update_bitbucket_metadata() -> Result<()> {
1518		use crate::remote::bitbucket::{
1519			BitbucketCommit,
1520			BitbucketCommitAuthor,
1521			BitbucketPullRequest,
1522			BitbucketPullRequestMergeCommit,
1523		};
1524
1525		let mut release = Release {
1526			version: None,
1527			message: None,
1528			extra: None,
1529			commits: vec![
1530				Commit::from(String::from(
1531					"1d244937ee6ceb8e0314a4a201ba93a7a61f2071 add bitbucket \
1532					 integration",
1533				)),
1534				Commit::from(String::from(
1535					"21f6aa587fcb772de13f2fde0e92697c51f84162 fix bitbucket \
1536					 integration",
1537				)),
1538				Commit::from(String::from(
1539					"35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973 update metadata",
1540				)),
1541				Commit::from(String::from(
1542					"4d3ffe4753b923f4d7807c490e650e6624a12074 do some stuff",
1543				)),
1544				Commit::from(String::from(
1545					"5a55e92e5a62dc5bf9872ffb2566959fad98bd05 alright",
1546				)),
1547				Commit::from(String::from(
1548					"6c34967147560ea09658776d4901709139b4ad66 should be fine",
1549				)),
1550			],
1551			commit_range: None,
1552			commit_id: None,
1553			timestamp: 0,
1554			previous: Some(Box::new(Release {
1555				version: Some(String::from("1.0.0")),
1556				..Default::default()
1557			})),
1558			repository: Some(String::from("/root/repo")),
1559			submodule_commits: HashMap::new(),
1560			#[cfg(feature = "github")]
1561			github: RemoteReleaseMetadata {
1562				contributors: vec![],
1563			},
1564			#[cfg(feature = "gitlab")]
1565			gitlab: RemoteReleaseMetadata {
1566				contributors: vec![],
1567			},
1568			#[cfg(feature = "gitea")]
1569			gitea: RemoteReleaseMetadata {
1570				contributors: vec![],
1571			},
1572			#[cfg(feature = "bitbucket")]
1573			bitbucket: RemoteReleaseMetadata {
1574				contributors: vec![],
1575			},
1576		};
1577		release.update_bitbucket_metadata(
1578			vec![
1579				BitbucketCommit {
1580					hash:   String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1581					author: Some(BitbucketCommitAuthor {
1582						login: Some(String::from("orhun")),
1583					}),
1584					date:   String::from("2021-07-18T15:14:39+03:00"),
1585				},
1586				BitbucketCommit {
1587					hash:   String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1588					author: Some(BitbucketCommitAuthor {
1589						login: Some(String::from("orhun")),
1590					}),
1591					date:   String::from("2021-07-18T15:12:19+03:00"),
1592				},
1593				BitbucketCommit {
1594					hash:   String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1595					author: Some(BitbucketCommitAuthor {
1596						login: Some(String::from("nuhro")),
1597					}),
1598					date:   String::from("2021-07-18T15:07:23+03:00"),
1599				},
1600				BitbucketCommit {
1601					hash:   String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1602					author: Some(BitbucketCommitAuthor {
1603						login: Some(String::from("awesome_contributor")),
1604					}),
1605					date:   String::from("2021-07-18T15:05:10+03:00"),
1606				},
1607				BitbucketCommit {
1608					hash:   String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1609					author: Some(BitbucketCommitAuthor {
1610						login: Some(String::from("orhun")),
1611					}),
1612					date:   String::from("2021-07-18T15:03:30+03:00"),
1613				},
1614				BitbucketCommit {
1615					hash:   String::from("6c34967147560ea09658776d4901709139b4ad66"),
1616					author: Some(BitbucketCommitAuthor {
1617						login: Some(String::from("someone")),
1618					}),
1619					date:   String::from("2021-07-18T15:00:38+03:00"),
1620				},
1621				BitbucketCommit {
1622					hash:   String::from("0c34967147560e809658776d4901709139b4ad68"),
1623					author: Some(BitbucketCommitAuthor {
1624						login: Some(String::from("idk")),
1625					}),
1626					date:   String::from("2021-07-18T15:00:01+03:00"),
1627				},
1628				BitbucketCommit {
1629					hash:   String::from("kk34967147560e809658776d4901709139b4ad68"),
1630					author: Some(BitbucketCommitAuthor {
1631						login: Some(String::from("orhun")),
1632					}),
1633					date:   String::from("2021-07-14T21:25:24+03:00"),
1634				},
1635			]
1636			.into_iter()
1637			.map(|v| Box::new(v) as Box<dyn RemoteCommit>)
1638			.collect(),
1639			vec![Box::new(BitbucketPullRequest {
1640				id:           1,
1641				title:        Some(String::from("1")),
1642				author:       BitbucketCommitAuthor {
1643					login: Some(String::from("42")),
1644				},
1645				merge_commit: BitbucketPullRequestMergeCommit {
1646					// Bitbucket merge commits returned in short format
1647					hash: String::from("1d244937ee6c"),
1648				},
1649			})],
1650		)?;
1651		#[allow(deprecated)]
1652		let expected_commits = vec![
1653			Commit {
1654				id: String::from("1d244937ee6ceb8e0314a4a201ba93a7a61f2071"),
1655				message: String::from("add bitbucket integration"),
1656				bitbucket: RemoteContributor {
1657					username:      Some(String::from("orhun")),
1658					pr_title:      Some(String::from("1")),
1659					pr_number:     Some(1),
1660					pr_labels:     vec![],
1661					is_first_time: false,
1662				},
1663				remote: Some(RemoteContributor {
1664					username:      Some(String::from("orhun")),
1665					pr_title:      Some(String::from("1")),
1666					pr_number:     Some(1),
1667					pr_labels:     vec![],
1668					is_first_time: false,
1669				}),
1670				..Default::default()
1671			},
1672			Commit {
1673				id: String::from("21f6aa587fcb772de13f2fde0e92697c51f84162"),
1674				message: String::from("fix bitbucket integration"),
1675				bitbucket: RemoteContributor {
1676					username:      Some(String::from("orhun")),
1677					pr_title:      None,
1678					pr_number:     None,
1679					pr_labels:     vec![],
1680					is_first_time: false,
1681				},
1682				remote: Some(RemoteContributor {
1683					username:      Some(String::from("orhun")),
1684					pr_title:      None,
1685					pr_number:     None,
1686					pr_labels:     vec![],
1687					is_first_time: false,
1688				}),
1689				..Default::default()
1690			},
1691			Commit {
1692				id: String::from("35d8c6b6329ecbcf131d7df02f93c3bbc5ba5973"),
1693				message: String::from("update metadata"),
1694				bitbucket: RemoteContributor {
1695					username:      Some(String::from("nuhro")),
1696					pr_title:      None,
1697					pr_number:     None,
1698					pr_labels:     vec![],
1699					is_first_time: false,
1700				},
1701				remote: Some(RemoteContributor {
1702					username:      Some(String::from("nuhro")),
1703					pr_title:      None,
1704					pr_number:     None,
1705					pr_labels:     vec![],
1706					is_first_time: false,
1707				}),
1708				..Default::default()
1709			},
1710			Commit {
1711				id: String::from("4d3ffe4753b923f4d7807c490e650e6624a12074"),
1712				message: String::from("do some stuff"),
1713				bitbucket: RemoteContributor {
1714					username:      Some(String::from("awesome_contributor")),
1715					pr_title:      None,
1716					pr_number:     None,
1717					pr_labels:     vec![],
1718					is_first_time: false,
1719				},
1720				remote: Some(RemoteContributor {
1721					username:      Some(String::from("awesome_contributor")),
1722					pr_title:      None,
1723					pr_number:     None,
1724					pr_labels:     vec![],
1725					is_first_time: false,
1726				}),
1727				..Default::default()
1728			},
1729			Commit {
1730				id: String::from("5a55e92e5a62dc5bf9872ffb2566959fad98bd05"),
1731				message: String::from("alright"),
1732				bitbucket: RemoteContributor {
1733					username:      Some(String::from("orhun")),
1734					pr_title:      None,
1735					pr_number:     None,
1736					pr_labels:     vec![],
1737					is_first_time: false,
1738				},
1739				remote: Some(RemoteContributor {
1740					username:      Some(String::from("orhun")),
1741					pr_title:      None,
1742					pr_number:     None,
1743					pr_labels:     vec![],
1744					is_first_time: false,
1745				}),
1746				..Default::default()
1747			},
1748			Commit {
1749				id: String::from("6c34967147560ea09658776d4901709139b4ad66"),
1750				message: String::from("should be fine"),
1751				bitbucket: RemoteContributor {
1752					username:      Some(String::from("someone")),
1753					pr_title:      None,
1754					pr_number:     None,
1755					pr_labels:     vec![],
1756					is_first_time: false,
1757				},
1758				remote: Some(RemoteContributor {
1759					username:      Some(String::from("someone")),
1760					pr_title:      None,
1761					pr_number:     None,
1762					pr_labels:     vec![],
1763					is_first_time: false,
1764				}),
1765				..Default::default()
1766			},
1767		];
1768		assert_eq!(expected_commits, release.commits);
1769
1770		release
1771			.bitbucket
1772			.contributors
1773			.sort_by(|a, b| a.pr_number.cmp(&b.pr_number));
1774
1775		let expected_metadata = RemoteReleaseMetadata {
1776			contributors: vec![
1777				RemoteContributor {
1778					username:      Some(String::from("nuhro")),
1779					pr_title:      None,
1780					pr_number:     None,
1781					pr_labels:     vec![],
1782					is_first_time: true,
1783				},
1784				RemoteContributor {
1785					username:      Some(String::from("awesome_contributor")),
1786					pr_title:      None,
1787					pr_number:     None,
1788					pr_labels:     vec![],
1789					is_first_time: true,
1790				},
1791				RemoteContributor {
1792					username:      Some(String::from("someone")),
1793					pr_title:      None,
1794					pr_number:     None,
1795					pr_labels:     vec![],
1796					is_first_time: true,
1797				},
1798				RemoteContributor {
1799					username:      Some(String::from("orhun")),
1800					pr_title:      Some(String::from("1")),
1801					pr_number:     Some(1),
1802					pr_labels:     vec![],
1803					is_first_time: false,
1804				},
1805			],
1806		};
1807		assert_eq!(expected_metadata, release.bitbucket);
1808
1809		Ok(())
1810	}
1811}