gnostr_asyncgit/sync/remotes/
callbacks.rs

1use std::sync::{
2	Arc, Mutex,
3	atomic::{AtomicBool, Ordering},
4};
5
6use crossbeam_channel::Sender;
7use git2::{Cred, Error as GitError, RemoteCallbacks};
8
9use super::push::ProgressNotification;
10use crate::{error::Result, sync::cred::BasicAuthCredential};
11
12///
13#[derive(Default, Clone)]
14pub struct CallbackStats {
15	pub push_rejected_msg: Option<(String, String)>,
16}
17
18///
19#[derive(Clone)]
20pub struct Callbacks {
21	sender: Option<Sender<ProgressNotification>>,
22	basic_credential: Option<BasicAuthCredential>,
23	stats: Arc<Mutex<CallbackStats>>,
24	first_call_to_credentials: Arc<AtomicBool>,
25}
26
27impl Callbacks {
28	///
29	pub fn new(
30		sender: Option<Sender<ProgressNotification>>,
31		basic_credential: Option<BasicAuthCredential>,
32	) -> Self {
33		let stats = Arc::new(Mutex::new(CallbackStats::default()));
34
35		Self {
36			sender,
37			basic_credential,
38			stats,
39			first_call_to_credentials: Arc::new(AtomicBool::new(
40				true,
41			)),
42		}
43	}
44
45	///
46	pub fn get_stats(&self) -> Result<CallbackStats> {
47		let stats = self.stats.lock()?;
48		Ok(stats.clone())
49	}
50
51	///
52	pub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {
53		let mut callbacks = RemoteCallbacks::new();
54
55		let this = self.clone();
56		callbacks.push_transfer_progress(
57			move |current, total, bytes| {
58				this.push_transfer_progress(current, total, bytes);
59			},
60		);
61
62		let this = self.clone();
63		callbacks.update_tips(move |name, a, b| {
64			this.update_tips(name, a, b);
65			true
66		});
67
68		let this = self.clone();
69		callbacks.transfer_progress(move |p| {
70			this.transfer_progress(&p);
71			true
72		});
73
74		let this = self.clone();
75		callbacks.pack_progress(move |stage, current, total| {
76			this.pack_progress(stage, total, current);
77		});
78
79		let this = self.clone();
80		callbacks.push_update_reference(move |reference, msg| {
81			this.push_update_reference(reference, msg);
82			Ok(())
83		});
84
85		let this = self.clone();
86		callbacks.credentials(
87			move |url, username_from_url, allowed_types| {
88				this.credentials(
89					url,
90					username_from_url,
91					allowed_types,
92				)
93			},
94		);
95
96		callbacks.sideband_progress(move |data| {
97			log::debug!(
98				"sideband transfer: '{}'",
99				String::from_utf8_lossy(data).trim()
100			);
101			true
102		});
103
104		callbacks
105	}
106
107	fn push_update_reference(
108		&self,
109		reference: &str,
110		msg: Option<&str>,
111	) {
112		log::debug!(
113			"push_update_reference: '{}' {:?}",
114			reference,
115			msg
116		);
117
118		if let Ok(mut stats) = self.stats.lock() {
119			stats.push_rejected_msg = msg
120				.map(|msg| (reference.to_string(), msg.to_string()));
121		}
122	}
123
124	fn pack_progress(
125		&self,
126		stage: git2::PackBuilderStage,
127		total: usize,
128		current: usize,
129	) {
130		log::debug!("packing: {:?} - {}/{}", stage, current, total);
131		self.sender.clone().map(|sender| {
132			sender.send(ProgressNotification::Packing {
133				stage,
134				total,
135				current,
136			})
137		});
138	}
139
140	fn transfer_progress(&self, p: &git2::Progress) {
141		log::debug!(
142			"transfer: {}/{}",
143			p.received_objects(),
144			p.total_objects()
145		);
146		self.sender.clone().map(|sender| {
147			sender.send(ProgressNotification::Transfer {
148				objects: p.received_objects(),
149				total_objects: p.total_objects(),
150			})
151		});
152	}
153
154	fn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {
155		log::debug!("update tips: '{}' [{}] [{}]", name, a, b);
156		self.sender.clone().map(|sender| {
157			sender.send(ProgressNotification::UpdateTips {
158				name: name.to_string(),
159				a: a.into(),
160				b: b.into(),
161			})
162		});
163	}
164
165	fn push_transfer_progress(
166		&self,
167		current: usize,
168		total: usize,
169		bytes: usize,
170	) {
171		log::debug!("progress: {}/{} ({} B)", current, total, bytes,);
172		self.sender.clone().map(|sender| {
173			sender.send(ProgressNotification::PushTransfer {
174				current,
175				total,
176				bytes,
177			})
178		});
179	}
180
181	// If credentials are bad, we don't ask the user to re-fill their
182	// creds. We push an error and they will be able to restart their
183	// action (for example a push) and retype their creds. This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347
184	// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588
185	// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/
186	fn credentials(
187		&self,
188		url: &str,
189		username_from_url: Option<&str>,
190		allowed_types: git2::CredentialType,
191	) -> std::result::Result<Cred, GitError> {
192		log::debug!(
193			"creds: '{}' {:?} ({:?})",
194			url,
195			username_from_url,
196			allowed_types
197		);
198
199		// This boolean is used to avoid multiple calls to credentials
200		// callback.
201		if self.first_call_to_credentials.load(Ordering::Relaxed) {
202			self.first_call_to_credentials
203				.store(false, Ordering::Relaxed);
204		} else {
205			return Err(GitError::from_str("Bad credentials."));
206		}
207
208		match &self.basic_credential {
209			_ if allowed_types.is_ssh_key() => username_from_url
210				.map_or_else(
211					|| {
212						Err(GitError::from_str(
213							" Couldn't extract username from url.",
214						))
215					},
216					Cred::ssh_key_from_agent,
217				),
218			Some(BasicAuthCredential {
219				username: Some(user),
220				password: Some(pwd),
221			}) if allowed_types.is_user_pass_plaintext() => {
222				Cred::userpass_plaintext(user, pwd)
223			}
224			Some(BasicAuthCredential {
225				username: Some(user),
226				password: _,
227			}) if allowed_types.is_username() => Cred::username(user),
228			_ if allowed_types.is_default() => Cred::default(),
229			_ => Err(GitError::from_str("Couldn't find credentials")),
230		}
231	}
232}