gnostr_asyncgit/sync/
hunks.rs

1use git2::{ApplyLocation, ApplyOptions, Diff};
2use scopetime::scope_time;
3
4use super::{
5    diff::{get_diff_raw, DiffOptions, HunkHeader},
6    RepoPath,
7};
8use crate::{
9    error::{Error, Result},
10    hash,
11    sync::repository::repo,
12};
13
14///
15pub fn stage_hunk(
16    repo_path: &RepoPath,
17    file_path: &str,
18    hunk_hash: u64,
19    options: Option<DiffOptions>,
20) -> Result<()> {
21    scope_time!("stage_hunk");
22
23    let repo = repo(repo_path)?;
24
25    let diff = get_diff_raw(&repo, file_path, false, false, options)?;
26
27    let mut opt = ApplyOptions::new();
28    opt.hunk_callback(|hunk| {
29        hunk.map_or(false, |hunk| {
30            let header = HunkHeader::from(hunk);
31            hash(&header) == hunk_hash
32        })
33    });
34
35    repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
36
37    Ok(())
38}
39
40/// this will fail for an all untracked file
41pub fn reset_hunk(
42    repo_path: &RepoPath,
43    file_path: &str,
44    hunk_hash: u64,
45    options: Option<DiffOptions>,
46) -> Result<()> {
47    scope_time!("reset_hunk");
48
49    let repo = repo(repo_path)?;
50
51    let diff = get_diff_raw(&repo, file_path, false, false, options)?;
52
53    let hunk_index = find_hunk_index(&diff, hunk_hash);
54    if let Some(hunk_index) = hunk_index {
55        let mut hunk_idx = 0;
56        let mut opt = ApplyOptions::new();
57        opt.hunk_callback(|_hunk| {
58            let res = hunk_idx == hunk_index;
59            hunk_idx += 1;
60            res
61        });
62
63        let diff = get_diff_raw(&repo, file_path, false, true, None)?;
64
65        repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
66
67        Ok(())
68    } else {
69        Err(Error::Generic("hunk not found".to_string()))
70    }
71}
72
73fn find_hunk_index(diff: &Diff, hunk_hash: u64) -> Option<usize> {
74    let mut result = None;
75
76    let mut hunk_count = 0;
77
78    let foreach_result = diff.foreach(
79        &mut |_, _| true,
80        None,
81        Some(&mut |_, hunk| {
82            let header = HunkHeader::from(hunk);
83            if hash(&header) == hunk_hash {
84                result = Some(hunk_count);
85            }
86            hunk_count += 1;
87            true
88        }),
89        None,
90    );
91
92    if foreach_result.is_ok() {
93        result
94    } else {
95        None
96    }
97}
98
99///
100pub fn unstage_hunk(
101    repo_path: &RepoPath,
102    file_path: &str,
103    hunk_hash: u64,
104    options: Option<DiffOptions>,
105) -> Result<bool> {
106    scope_time!("revert_hunk");
107
108    let repo = repo(repo_path)?;
109
110    let diff = get_diff_raw(&repo, file_path, true, false, options)?;
111    let diff_count_positive = diff.deltas().len();
112
113    let hunk_index = find_hunk_index(&diff, hunk_hash);
114    let hunk_index =
115        hunk_index.map_or_else(|| Err(Error::Generic("hunk not found".to_string())), Ok)?;
116
117    let diff = get_diff_raw(&repo, file_path, true, true, options)?;
118
119    if diff.deltas().len() != diff_count_positive {
120        return Err(Error::Generic(format!(
121            "hunk error: {}!={}",
122            diff.deltas().len(),
123            diff_count_positive
124        )));
125    }
126
127    let mut count = 0;
128    {
129        let mut hunk_idx = 0;
130        let mut opt = ApplyOptions::new();
131        opt.hunk_callback(|_hunk| {
132            let res = if hunk_idx == hunk_index {
133                count += 1;
134                true
135            } else {
136                false
137            };
138
139            hunk_idx += 1;
140
141            res
142        });
143
144        repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
145    }
146
147    Ok(count == 1)
148}
149
150#[cfg(test)]
151mod tests {
152    use std::{
153        fs::{self, File},
154        io::Write,
155        path::Path,
156    };
157
158    use super::*;
159    use crate::{
160        error::Result,
161        sync::{diff::get_diff, tests::repo_init_empty},
162    };
163
164    #[test]
165    fn reset_untracked_file_which_will_not_find_hunk() -> Result<()> {
166        let file_path = Path::new("foo/foo.txt");
167        let (_td, repo) = repo_init_empty()?;
168        let root = repo.path().parent().unwrap();
169        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
170        let sub_path = root.join("foo/");
171
172        fs::create_dir_all(&sub_path)?;
173        File::create(root.join(file_path))?.write_all(b"test")?;
174
175        let sub_path: &RepoPath = &sub_path.to_str().unwrap().into();
176        let diff = get_diff(sub_path, file_path.to_str().unwrap(), false, None)?;
177
178        assert!(reset_hunk(
179            repo_path,
180            file_path.to_str().unwrap(),
181            diff.hunks[0].header_hash,
182            None,
183        )
184        .is_err());
185
186        Ok(())
187    }
188}