gnostr_asyncgit/sync/
hunks.rs

1use git2::{ApplyLocation, ApplyOptions, Diff};
2use scopetime::scope_time;
3
4use super::{
5	RepoPath,
6	diff::{DiffOptions, HunkHeader, get_diff_raw},
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() { result } else { None }
93}
94
95///
96pub fn unstage_hunk(
97	repo_path: &RepoPath,
98	file_path: &str,
99	hunk_hash: u64,
100	options: Option<DiffOptions>,
101) -> Result<bool> {
102	scope_time!("revert_hunk");
103
104	let repo = repo(repo_path)?;
105
106	let diff = get_diff_raw(&repo, file_path, true, false, options)?;
107	let diff_count_positive = diff.deltas().len();
108
109	let hunk_index = find_hunk_index(&diff, hunk_hash);
110	let hunk_index = hunk_index.map_or_else(
111		|| Err(Error::Generic("hunk not found".to_string())),
112		Ok,
113	)?;
114
115	let diff = get_diff_raw(&repo, file_path, true, true, options)?;
116
117	if diff.deltas().len() != diff_count_positive {
118		return Err(Error::Generic(format!(
119			"hunk error: {}!={}",
120			diff.deltas().len(),
121			diff_count_positive
122		)));
123	}
124
125	let mut count = 0;
126	{
127		let mut hunk_idx = 0;
128		let mut opt = ApplyOptions::new();
129		opt.hunk_callback(|_hunk| {
130			let res = if hunk_idx == hunk_index {
131				count += 1;
132				true
133			} else {
134				false
135			};
136
137			hunk_idx += 1;
138
139			res
140		});
141
142		repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
143	}
144
145	Ok(count == 1)
146}
147
148#[cfg(test)]
149mod tests {
150	use std::{
151		fs::{self, File},
152		io::Write,
153		path::Path,
154	};
155
156	use super::*;
157	use crate::{
158		error::Result,
159		sync::{diff::get_diff, tests::repo_init_empty},
160	};
161
162	#[test]
163	fn reset_untracked_file_which_will_not_find_hunk() -> Result<()> {
164		let file_path = Path::new("foo/foo.txt");
165		let (_td, repo) = repo_init_empty()?;
166		let root = repo.path().parent().unwrap();
167		let repo_path: &RepoPath =
168			&root.as_os_str().to_str().unwrap().into();
169		let sub_path = root.join("foo/");
170
171		fs::create_dir_all(&sub_path)?;
172		File::create(root.join(file_path))?.write_all(b"test")?;
173
174		let sub_path: &RepoPath = &sub_path.to_str().unwrap().into();
175		let diff = get_diff(
176			sub_path,
177			file_path.to_str().unwrap(),
178			false,
179			None,
180		)?;
181
182		assert!(
183			reset_hunk(
184				repo_path,
185				file_path.to_str().unwrap(),
186				diff.hunks[0].header_hash,
187				None,
188			)
189			.is_err()
190		);
191
192		Ok(())
193	}
194}