1use super::push::AsyncProgress;
4use crate::{
5	error::Result,
6	progress::ProgressPercent,
7	sync::{
8		cred::BasicAuthCredential,
9		remotes::{proxy_auto, Callbacks},
10		repository::repo,
11		RepoPath,
12	},
13};
14use crossbeam_channel::Sender;
15use git2::{Direction, PushOptions};
16use scopetime::scope_time;
17use std::collections::HashSet;
18
19#[derive(Debug, Copy, Clone, PartialEq, Eq)]
21pub enum PushTagsProgress {
22	CheckRemote,
24	Push {
26		pushed: usize,
28		total: usize,
30	},
31	Done,
33}
34
35impl AsyncProgress for PushTagsProgress {
36	fn progress(&self) -> ProgressPercent {
37		match self {
38			Self::CheckRemote => ProgressPercent::empty(),
39			Self::Push { pushed, total } => {
40				ProgressPercent::new(*pushed, *total)
41			}
42			Self::Done => ProgressPercent::full(),
43		}
44	}
45	fn is_done(&self) -> bool {
46		*self == Self::Done
47	}
48}
49
50fn remote_tag_refs(
52	repo_path: &RepoPath,
53	remote: &str,
54	basic_credential: Option<BasicAuthCredential>,
55) -> Result<Vec<String>> {
56	scope_time!("remote_tags");
57
58	let repo = repo(repo_path)?;
59	let mut remote = repo.find_remote(remote)?;
60	let callbacks = Callbacks::new(None, basic_credential);
61	let conn = remote.connect_auth(
62		Direction::Fetch,
63		Some(callbacks.callbacks()),
64		Some(proxy_auto()),
65	)?;
66
67	let remote_heads = conn.list()?;
68	let remote_tags = remote_heads
69		.iter()
70		.map(|s| s.name().to_string())
71		.filter(|name| {
72			name.starts_with("refs/tags/") && !name.ends_with("^{}")
73		})
74		.collect::<Vec<_>>();
75
76	Ok(remote_tags)
77}
78
79pub fn tags_missing_remote(
81	repo_path: &RepoPath,
82	remote: &str,
83	basic_credential: Option<BasicAuthCredential>,
84) -> Result<Vec<String>> {
85	scope_time!("tags_missing_remote");
86
87	let repo = repo(repo_path)?;
88	let tags = repo.tag_names(None)?;
89
90	let mut local_tags = tags
91		.iter()
92		.filter_map(|tag| tag.map(|tag| format!("refs/tags/{tag}")))
93		.collect::<HashSet<_>>();
94	let remote_tags =
95		remote_tag_refs(repo_path, remote, basic_credential)?;
96
97	for t in remote_tags {
98		local_tags.remove(&t);
99	}
100
101	Ok(local_tags.into_iter().collect())
102}
103
104pub fn push_tags(
106	repo_path: &RepoPath,
107	remote: &str,
108	basic_credential: Option<BasicAuthCredential>,
109	progress_sender: Option<Sender<PushTagsProgress>>,
110) -> Result<()> {
111	scope_time!("push_tags");
112
113	progress_sender
114		.as_ref()
115		.map(|sender| sender.send(PushTagsProgress::CheckRemote));
116
117	let tags_missing = tags_missing_remote(
118		repo_path,
119		remote,
120		basic_credential.clone(),
121	)?;
122
123	let repo = repo(repo_path)?;
124	let mut remote = repo.find_remote(remote)?;
125
126	let total = tags_missing.len();
127
128	progress_sender.as_ref().map(|sender| {
129		sender.send(PushTagsProgress::Push { pushed: 0, total })
130	});
131
132	for (idx, tag) in tags_missing.into_iter().enumerate() {
133		let mut options = PushOptions::new();
134		let callbacks =
135			Callbacks::new(None, basic_credential.clone());
136		options.remote_callbacks(callbacks.callbacks());
137		options.packbuilder_parallelism(0);
138		options.proxy_options(proxy_auto());
139		remote.push(&[tag.as_str()], Some(&mut options))?;
140
141		progress_sender.as_ref().map(|sender| {
142			sender.send(PushTagsProgress::Push {
143				pushed: idx + 1,
144				total,
145			})
146		});
147	}
148
149	drop(basic_credential);
150
151	progress_sender.map(|sender| sender.send(PushTagsProgress::Done));
152
153	Ok(())
154}
155
156#[cfg(test)]
157mod tests {
158	use super::*;
159	use crate::{
160		sync::{
161			self, delete_tag,
162			remotes::{
163				fetch, fetch_all,
164				push::{push_branch, push_raw},
165			},
166			tests::{repo_clone, repo_init_bare},
167		},
168		PushType,
169	};
170	use pretty_assertions::assert_eq;
171	use sync::tests::write_commit_file;
172
173	#[test]
174	fn test_push_pull_tags() {
175		let (r1_dir, _repo) = repo_init_bare().unwrap();
176		let r1_dir = r1_dir.path().to_str().unwrap();
177
178		let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();
179
180		let clone1_dir: &RepoPath =
181			&clone1_dir.path().to_str().unwrap().into();
182
183		let (clone2_dir, clone2) = repo_clone(r1_dir).unwrap();
184
185		let clone2_dir: &RepoPath =
186			&clone2_dir.path().to_str().unwrap().into();
187
188		let commit1 =
191			write_commit_file(&clone1, "test.txt", "test", "commit1");
192
193		sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap();
194
195		push_branch(
196			clone1_dir, "origin", "master", false, false, None, None,
197		)
198		.unwrap();
199		push_tags(clone1_dir, "origin", None, None).unwrap();
200
201		let _commit2 = write_commit_file(
204			&clone2,
205			"test2.txt",
206			"test",
207			"commit2",
208		);
209
210		assert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 0);
211
212		let bytes = fetch(clone2_dir, "master", None, None).unwrap();
214		assert!(bytes > 0);
215
216		sync::merge_upstream_commit(clone2_dir, "master").unwrap();
217
218		assert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 1);
219	}
220
221	#[test]
222	fn test_get_remote_tags() {
223		let (r1_dir, _repo) = repo_init_bare().unwrap();
224		let r1_dir = r1_dir.path().to_str().unwrap();
225
226		let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();
227
228		let clone1_dir: &RepoPath =
229			&clone1_dir.path().to_str().unwrap().into();
230
231		let (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();
232
233		let clone2_dir: &RepoPath =
234			&clone2_dir.path().to_str().unwrap().into();
235
236		let commit1 =
239			write_commit_file(&clone1, "test.txt", "test", "commit1");
240
241		sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap();
242
243		push_branch(
244			clone1_dir, "origin", "master", false, false, None, None,
245		)
246		.unwrap();
247		push_tags(clone1_dir, "origin", None, None).unwrap();
248
249		let tags =
252			remote_tag_refs(clone2_dir, "origin", None).unwrap();
253
254		assert_eq!(
255			tags.as_slice(),
256			&[String::from("refs/tags/tag1")]
257		);
258	}
259
260	#[test]
261	fn test_tags_missing_remote() {
262		let (r1_dir, _repo) = repo_init_bare().unwrap();
263		let r1_dir = r1_dir.path().to_str().unwrap();
264
265		let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();
266
267		let clone1_dir: &RepoPath =
268			&clone1_dir.path().to_str().unwrap().into();
269
270		let commit1 =
273			write_commit_file(&clone1, "test.txt", "test", "commit1");
274
275		sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap();
276
277		push_branch(
278			clone1_dir, "origin", "master", false, false, None, None,
279		)
280		.unwrap();
281
282		let tags_missing =
283			tags_missing_remote(clone1_dir, "origin", None).unwrap();
284
285		assert_eq!(
286			tags_missing.as_slice(),
287			&[String::from("refs/tags/tag1")]
288		);
289		push_tags(clone1_dir, "origin", None, None).unwrap();
290		let tags_missing =
291			tags_missing_remote(clone1_dir, "origin", None).unwrap();
292		assert!(tags_missing.is_empty());
293	}
294
295	#[test]
296	fn test_tags_fetch() {
297		let (r1_dir, _repo) = repo_init_bare().unwrap();
298		let r1_dir = r1_dir.path().to_str().unwrap();
299
300		let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();
301		let clone1_dir: &RepoPath =
302			&clone1_dir.path().to_str().unwrap().into();
303
304		let commit1 =
305			write_commit_file(&clone1, "test.txt", "test", "commit1");
306		push_branch(
307			clone1_dir, "origin", "master", false, false, None, None,
308		)
309		.unwrap();
310
311		let (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();
312		let clone2_dir: &RepoPath =
313			&clone2_dir.path().to_str().unwrap().into();
314
315		sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap();
318
319		let tags1 = sync::get_tags(clone1_dir).unwrap();
320
321		push_tags(clone1_dir, "origin", None, None).unwrap();
322		let tags_missing =
323			tags_missing_remote(clone1_dir, "origin", None).unwrap();
324		assert!(tags_missing.is_empty());
325
326		fetch(clone2_dir, "master", None, None).unwrap();
329
330		let tags2 = sync::get_tags(clone2_dir).unwrap();
331
332		assert_eq!(tags1, tags2);
333	}
334
335	#[test]
336	fn test_tags_fetch_all() {
337		let (r1_dir, _repo) = repo_init_bare().unwrap();
338		let r1_dir = r1_dir.path().to_str().unwrap();
339
340		let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();
341		let clone1_dir: &RepoPath =
342			&clone1_dir.path().to_str().unwrap().into();
343
344		let commit1 =
345			write_commit_file(&clone1, "test.txt", "test", "commit1");
346		push_branch(
347			clone1_dir, "origin", "master", false, false, None, None,
348		)
349		.unwrap();
350
351		let (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();
352		let clone2_dir: &RepoPath =
353			&clone2_dir.path().to_str().unwrap().into();
354
355		sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap();
358
359		let tags1 = sync::get_tags(clone1_dir).unwrap();
360
361		push_tags(clone1_dir, "origin", None, None).unwrap();
362		let tags_missing =
363			tags_missing_remote(clone1_dir, "origin", None).unwrap();
364		assert!(tags_missing.is_empty());
365
366		fetch_all(clone2_dir, &None, &None).unwrap();
369
370		let tags2 = sync::get_tags(clone2_dir).unwrap();
371
372		assert_eq!(tags1, tags2);
373	}
374
375	#[test]
376	fn test_tags_delete_remote() {
377		let (r1_dir, _repo) = repo_init_bare().unwrap();
378		let r1_dir = r1_dir.path().to_str().unwrap();
379
380		let (clone1_dir, clone1) = repo_clone(r1_dir).unwrap();
381		let clone1_dir: &RepoPath =
382			&clone1_dir.path().to_str().unwrap().into();
383
384		let commit1 =
385			write_commit_file(&clone1, "test.txt", "test", "commit1");
386		push_branch(
387			clone1_dir, "origin", "master", false, false, None, None,
388		)
389		.unwrap();
390
391		let (clone2_dir, _clone2) = repo_clone(r1_dir).unwrap();
392		let clone2_dir: &RepoPath =
393			&clone2_dir.path().to_str().unwrap().into();
394
395		sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap();
398		push_tags(clone1_dir, "origin", None, None).unwrap();
399
400		fetch_all(clone2_dir, &None, &None).unwrap();
403		assert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 1);
404
405		delete_tag(clone1_dir, "tag1").unwrap();
408
409		push_raw(
410			clone1_dir,
411			"origin",
412			"tag1",
413			PushType::Tag,
414			false,
415			true,
416			None,
417			None,
418		)
419		.unwrap();
420
421		push_tags(clone1_dir, "origin", None, None).unwrap();
422
423		fetch_all(clone2_dir, &None, &None).unwrap();
426		assert_eq!(sync::get_tags(clone2_dir).unwrap().len(), 0);
427	}
428}