gnostr_asyncgit/sync/
mod.rs

1//! sync git api
2
3//TODO: remove once we have this activated on the toplevel
4#![deny(clippy::expect_used)]
5
6pub mod blame;
7pub mod branch;
8pub mod commit;
9mod commit_details;
10pub mod commit_files;
11mod commit_filter;
12mod commit_revert;
13mod commits_info;
14mod config;
15pub mod cred;
16pub mod diff;
17mod hooks;
18mod hunks;
19mod ignore;
20mod logwalker;
21mod merge;
22mod patches;
23mod rebase;
24pub mod remotes;
25mod repository;
26mod reset;
27mod reword;
28pub mod sign;
29mod staging;
30mod stash;
31mod state;
32pub mod status;
33mod submodules;
34mod tags;
35mod tree;
36pub mod utils;
37
38pub use blame::{BlameHunk, FileBlame, blame_file};
39pub use branch::{
40	BranchCompare, BranchDetails, BranchInfo,
41	branch_compare_upstream, checkout_branch, checkout_commit,
42	config_is_pull_rebase, create_branch, delete_branch,
43	get_branch_remote, get_branches_info,
44	merge_commit::merge_upstream_commit,
45	merge_ff::branch_merge_upstream_fastforward,
46	merge_rebase::merge_upstream_rebase, rename::rename_branch,
47	validate_branch_name,
48};
49pub use commit::{amend, commit, tag_commit};
50pub use commit_details::{
51	CommitDetails, CommitMessage, CommitSignature, get_commit_details,
52};
53pub use commit_files::get_commit_files;
54pub use commit_filter::{
55	LogFilterSearch, LogFilterSearchOptions, SearchFields,
56	SearchOptions, SharedCommitFilterFn, diff_contains_file,
57	filter_commit_by_search,
58};
59pub use commit_revert::{commit_revert, revert_commit, revert_head};
60pub use commits_info::{
61	CommitId, CommitInfo, get_commit_info, get_commits_info,
62};
63pub use config::{
64	ShowUntrackedFilesConfig, get_config_string,
65	untracked_files_config,
66};
67pub use diff::get_diff_commit;
68pub use git2::{BranchType, ResetType};
69pub use hooks::{
70	HookResult, PrepareCommitMsgSource, hooks_commit_msg,
71	hooks_post_commit, hooks_pre_commit, hooks_prepare_commit_msg,
72};
73pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
74pub use ignore::add_to_ignore;
75pub use logwalker::LogWalker;
76pub use merge::{
77	abort_pending_rebase, abort_pending_state,
78	continue_pending_rebase, merge_branch, merge_commit, merge_msg,
79	mergehead_ids, rebase_progress,
80};
81pub use rebase::rebase_branch;
82pub use remotes::{
83	get_default_remote, get_default_remote_for_fetch,
84	get_default_remote_for_push, get_remotes, push::AsyncProgress,
85	tags::PushTagsProgress,
86};
87pub(crate) use repository::repo;
88pub use repository::{RepoPath, RepoPathRef};
89pub use reset::{reset_repo, reset_stage, reset_workdir};
90pub use reword::reword;
91pub use staging::{discard_lines, stage_lines};
92pub use stash::{
93	get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
94};
95pub use state::{RepoState, repo_state};
96pub use status::is_workdir_clean;
97pub use submodules::{
98	SubmoduleInfo, SubmoduleParentInfo, SubmoduleStatus,
99	get_submodules, submodule_parent_info, update_submodule,
100};
101pub use tags::{
102	CommitTags, Tag, TagWithMetadata, Tags, delete_tag, get_tags,
103	get_tags_with_metadata,
104};
105pub use tree::{TreeFile, tree_file_content, tree_files};
106pub use utils::{
107	Head, get_head, get_head_tuple, repo_dir, repo_open_error,
108	stage_add_all, stage_add_file, stage_addremoved,
109};
110
111#[cfg(test)]
112mod tests {
113	use std::{path::Path, process::Command};
114
115	use git2::Repository;
116	use tempfile::TempDir;
117
118	use super::{
119		CommitId, LogWalker, RepoPath, commit,
120		repository::repo,
121		stage_add_file,
122		status::{StatusType, get_status},
123		utils::{get_head_repo, repo_write_file},
124	};
125	use crate::error::Result;
126
127	/// Calling `set_search_path` with an empty directory makes sure
128	/// that there is no git config interfering with our tests (for
129	/// example user-local `.gitconfig`).
130	#[allow(unsafe_code)]
131	fn sandbox_config_files() {
132		use std::sync::Once;
133
134		use git2::{ConfigLevel, opts::set_search_path};
135
136		static INIT: Once = Once::new();
137
138		// Adapted from https://github.com/rust-lang/cargo/pull/9035
139		INIT.call_once(|| unsafe {
140			let temp_dir = TempDir::new().unwrap();
141			let path = temp_dir.path();
142
143			set_search_path(ConfigLevel::System, path).unwrap();
144			set_search_path(ConfigLevel::Global, path).unwrap();
145			set_search_path(ConfigLevel::XDG, path).unwrap();
146			set_search_path(ConfigLevel::ProgramData, path).unwrap();
147		});
148	}
149
150	/// write, stage and commit a file
151	pub fn write_commit_file(
152		repo: &Repository,
153		file: &str,
154		content: &str,
155		commit_name: &str,
156	) -> CommitId {
157		repo_write_file(repo, file, content).unwrap();
158
159		stage_add_file(
160			&repo.workdir().unwrap().to_str().unwrap().into(),
161			Path::new(file),
162		)
163		.unwrap();
164
165		commit(
166			&repo.workdir().unwrap().to_str().unwrap().into(),
167			commit_name,
168		)
169		.unwrap()
170	}
171
172	/// write, stage and commit a file giving the commit a specific
173	/// timestamp
174	pub fn write_commit_file_at(
175		repo: &Repository,
176		file: &str,
177		content: &str,
178		commit_name: &str,
179		time: git2::Time,
180	) -> CommitId {
181		repo_write_file(repo, file, content).unwrap();
182
183		let path: &RepoPath =
184			&repo.workdir().unwrap().to_str().unwrap().into();
185
186		stage_add_file(path, Path::new(file)).unwrap();
187
188		commit_at(path, commit_name, time)
189	}
190
191	fn commit_at(
192		repo_path: &RepoPath,
193		msg: &str,
194		time: git2::Time,
195	) -> CommitId {
196		let repo = repo(repo_path).unwrap();
197
198		let signature =
199			git2::Signature::new("name", "email", &time).unwrap();
200		let mut index = repo.index().unwrap();
201		let tree_id = index.write_tree().unwrap();
202		let tree = repo.find_tree(tree_id).unwrap();
203
204		let parents = if let Ok(id) = get_head_repo(&repo) {
205			vec![repo.find_commit(id.into()).unwrap()]
206		} else {
207			Vec::new()
208		};
209
210		let parents = parents.iter().collect::<Vec<_>>();
211
212		let commit = repo
213			.commit(
214				Some("HEAD"),
215				&signature,
216				&signature,
217				msg,
218				&tree,
219				parents.as_slice(),
220			)
221			.unwrap()
222			.into();
223
224		commit
225	}
226
227	///
228	pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
229		init_log();
230
231		sandbox_config_files();
232
233		let td = TempDir::new()?;
234		let repo = Repository::init(td.path())?;
235		{
236			let mut config = repo.config()?;
237			config.set_str("user.name", "name")?;
238			config.set_str("user.email", "email")?;
239		}
240		Ok((td, repo))
241	}
242
243	///
244	pub fn repo_init() -> Result<(TempDir, Repository)> {
245		init_log();
246
247		sandbox_config_files();
248
249		let td = TempDir::new()?;
250		let repo = Repository::init(td.path())?;
251		{
252			let mut config = repo.config()?;
253			config.set_str("user.name", "name")?;
254			config.set_str("user.email", "email")?;
255
256			let mut index = repo.index()?;
257			let id = index.write_tree()?;
258
259			let tree = repo.find_tree(id)?;
260			let sig = repo.signature()?;
261			repo.commit(
262				Some("HEAD"),
263				&sig,
264				&sig,
265				"initial",
266				&tree,
267				&[],
268			)?;
269		}
270		Ok((td, repo))
271	}
272
273	///
274	pub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> {
275		sandbox_config_files();
276
277		let td = TempDir::new()?;
278
279		let td_path = td.path().as_os_str().to_str().unwrap();
280
281		let repo = Repository::clone(p, td_path).unwrap();
282
283		let mut config = repo.config()?;
284		config.set_str("user.name", "name")?;
285		config.set_str("user.email", "email")?;
286
287		Ok((td, repo))
288	}
289
290	// init log
291	fn init_log() {
292		let _ = env_logger::builder()
293			.is_test(true)
294			.filter_level(log::LevelFilter::Trace)
295			.try_init();
296	}
297
298	/// Same as `repo_init`, but the repo is a bare repo (--bare)
299	pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
300		init_log();
301
302		let tmp_repo_dir = TempDir::new()?;
303		let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
304		Ok((tmp_repo_dir, bare_repo))
305	}
306
307	/// helper returning amount of files with changes in the
308	/// (wd,stage)
309	pub fn get_statuses(repo_path: &RepoPath) -> (usize, usize) {
310		(
311			get_status(repo_path, StatusType::WorkingDir, None)
312				.unwrap()
313				.len(),
314			get_status(repo_path, StatusType::Stage, None)
315				.unwrap()
316				.len(),
317		)
318	}
319
320	///
321	pub fn debug_cmd_print(path: &RepoPath, cmd: &str) {
322		let cmd = debug_cmd(path, cmd);
323		eprintln!("\n----\n{cmd}");
324	}
325
326	/// helper to fetch commit details using log walker
327	pub fn get_commit_ids(
328		r: &Repository,
329		max_count: usize,
330	) -> Vec<CommitId> {
331		let mut commit_ids = Vec::<CommitId>::new();
332		LogWalker::new(r, max_count)
333			.unwrap()
334			.read(&mut commit_ids)
335			.unwrap();
336
337		commit_ids
338	}
339
340	fn debug_cmd(path: &RepoPath, cmd: &str) -> String {
341		let output = if cfg!(target_os = "windows") {
342			Command::new("cmd")
343				.args(["/C", cmd])
344				.current_dir(path.gitpath())
345				.output()
346				.unwrap()
347		} else {
348			Command::new("sh")
349				.arg("-c")
350				.arg(cmd)
351				.current_dir(path.gitpath())
352				.output()
353				.unwrap()
354		};
355
356		let stdout = String::from_utf8_lossy(&output.stdout);
357		let stderr = String::from_utf8_lossy(&output.stderr);
358		format!(
359			"{}{}",
360			if stdout.is_empty() {
361				String::new()
362			} else {
363				format!("out:\n{stdout}")
364			},
365			if stderr.is_empty() {
366				String::new()
367			} else {
368				format!("err:\n{stderr}")
369			}
370		)
371	}
372}