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