1#![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 #[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 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 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 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 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 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 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 fn init_log() {
292 let _ = env_logger::builder()
293 .is_test(true)
294 .filter_level(log::LevelFilter::Trace)
295 .try_init();
296 }
297
298 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 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 pub fn debug_cmd_print(path: &RepoPath, cmd: &str) {
322 let cmd = debug_cmd(path, cmd);
323 eprintln!("\n----\n{cmd}");
324 }
325
326 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}