1use crate::build::CheckoutBuilder;
2use crate::util::{self, Binding};
3use crate::{panic, raw, IntoCString, Oid, Signature, StashApplyProgress, StashFlags};
4use libc::{c_char, c_int, c_void, size_t};
5use std::ffi::{c_uint, CStr, CString};
6use std::mem;
7
8pub struct StashSaveOptions<'a> {
10 message: Option<CString>,
11 flags: Option<StashFlags>,
12 stasher: Signature<'a>,
13 pathspec: Vec<CString>,
14 pathspec_ptrs: Vec<*const c_char>,
15 raw_opts: raw::git_stash_save_options,
16}
17
18impl<'a> StashSaveOptions<'a> {
19 pub fn new(stasher: Signature<'a>) -> Self {
21 let mut opts = Self {
22 message: None,
23 flags: None,
24 stasher,
25 pathspec: Vec::new(),
26 pathspec_ptrs: Vec::new(),
27 raw_opts: unsafe { mem::zeroed() },
28 };
29 assert_eq!(
30 unsafe {
31 raw::git_stash_save_options_init(
32 &mut opts.raw_opts,
33 raw::GIT_STASH_SAVE_OPTIONS_VERSION,
34 )
35 },
36 0
37 );
38 opts
39 }
40
41 pub fn flags(&mut self, flags: Option<StashFlags>) -> &mut Self {
43 self.flags = flags;
44 self
45 }
46
47 pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut Self {
49 let s = util::cstring_to_repo_path(pathspec).unwrap();
50 self.pathspec_ptrs.push(s.as_ptr());
51 self.pathspec.push(s);
52 self
53 }
54
55 pub unsafe fn raw(&mut self) -> *const raw::git_stash_save_options {
60 self.raw_opts.flags = self.flags.unwrap_or_else(StashFlags::empty).bits() as c_uint;
61 self.raw_opts.message = crate::call::convert(&self.message);
62 self.raw_opts.paths.count = self.pathspec_ptrs.len() as size_t;
63 self.raw_opts.paths.strings = self.pathspec_ptrs.as_ptr() as *mut _;
64 self.raw_opts.stasher = self.stasher.raw();
65
66 &self.raw_opts as *const _
67 }
68}
69
70pub type StashApplyProgressCb<'a> = dyn FnMut(StashApplyProgress) -> bool + 'a;
76
77pub type StashCb<'a> = dyn FnMut(usize, &str, &Oid) -> bool + 'a;
81
82pub struct StashApplyOptions<'cb> {
84 progress: Option<Box<StashApplyProgressCb<'cb>>>,
85 checkout_options: Option<CheckoutBuilder<'cb>>,
86 raw_opts: raw::git_stash_apply_options,
87}
88
89impl<'cb> Default for StashApplyOptions<'cb> {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl<'cb> StashApplyOptions<'cb> {
96 pub fn new() -> StashApplyOptions<'cb> {
98 let mut opts = StashApplyOptions {
99 progress: None,
100 checkout_options: None,
101 raw_opts: unsafe { mem::zeroed() },
102 };
103 assert_eq!(
104 unsafe { raw::git_stash_apply_init_options(&mut opts.raw_opts, 1) },
105 0
106 );
107 opts
108 }
109
110 pub fn reinstantiate_index(&mut self) -> &mut StashApplyOptions<'cb> {
112 self.raw_opts.flags = raw::GIT_STASH_APPLY_REINSTATE_INDEX as u32;
113 self
114 }
115
116 pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut StashApplyOptions<'cb> {
118 self.checkout_options = Some(opts);
119 self
120 }
121
122 pub fn progress_cb<C>(&mut self, callback: C) -> &mut StashApplyOptions<'cb>
127 where
128 C: FnMut(StashApplyProgress) -> bool + 'cb,
129 {
130 self.progress = Some(Box::new(callback) as Box<StashApplyProgressCb<'cb>>);
131 self.raw_opts.progress_cb = Some(stash_apply_progress_cb);
132 self.raw_opts.progress_payload = self as *mut _ as *mut _;
133 self
134 }
135
136 pub fn raw(&mut self) -> &raw::git_stash_apply_options {
138 unsafe {
139 if let Some(opts) = self.checkout_options.as_mut() {
140 opts.configure(&mut self.raw_opts.checkout_options);
141 }
142 }
143 &self.raw_opts
144 }
145}
146
147pub(crate) struct StashCbData<'a> {
148 pub callback: &'a mut StashCb<'a>,
149}
150
151pub(crate) extern "C" fn stash_cb(
152 index: size_t,
153 message: *const c_char,
154 stash_id: *const raw::git_oid,
155 payload: *mut c_void,
156) -> c_int {
157 panic::wrap(|| unsafe {
158 let data = &mut *(payload as *mut StashCbData<'_>);
159 let res = {
160 let callback = &mut data.callback;
161 callback(
162 index,
163 CStr::from_ptr(message).to_str().unwrap(),
164 &Binding::from_raw(stash_id),
165 )
166 };
167
168 if res {
169 0
170 } else {
171 1
172 }
173 })
174 .unwrap_or(1)
175}
176
177fn convert_progress(progress: raw::git_stash_apply_progress_t) -> StashApplyProgress {
178 match progress {
179 raw::GIT_STASH_APPLY_PROGRESS_NONE => StashApplyProgress::None,
180 raw::GIT_STASH_APPLY_PROGRESS_LOADING_STASH => StashApplyProgress::LoadingStash,
181 raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_INDEX => StashApplyProgress::AnalyzeIndex,
182 raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_MODIFIED => StashApplyProgress::AnalyzeModified,
183 raw::GIT_STASH_APPLY_PROGRESS_ANALYZE_UNTRACKED => StashApplyProgress::AnalyzeUntracked,
184 raw::GIT_STASH_APPLY_PROGRESS_CHECKOUT_UNTRACKED => StashApplyProgress::CheckoutUntracked,
185 raw::GIT_STASH_APPLY_PROGRESS_CHECKOUT_MODIFIED => StashApplyProgress::CheckoutModified,
186 raw::GIT_STASH_APPLY_PROGRESS_DONE => StashApplyProgress::Done,
187
188 _ => StashApplyProgress::None,
189 }
190}
191
192extern "C" fn stash_apply_progress_cb(
193 progress: raw::git_stash_apply_progress_t,
194 payload: *mut c_void,
195) -> c_int {
196 panic::wrap(|| unsafe {
197 let options = &mut *(payload as *mut StashApplyOptions<'_>);
198 let res = {
199 let callback = options.progress.as_mut().unwrap();
200 callback(convert_progress(progress))
201 };
202
203 if res {
204 0
205 } else {
206 -1
207 }
208 })
209 .unwrap_or(-1)
210}
211
212#[cfg(test)]
213mod tests {
214 use crate::stash::{StashApplyOptions, StashSaveOptions};
215 use crate::test::repo_init;
216 use crate::{IndexAddOption, Repository, StashFlags, Status};
217 use std::fs;
218 use std::path::{Path, PathBuf};
219
220 fn make_stash<C>(next: C)
221 where
222 C: FnOnce(&mut Repository),
223 {
224 let (_td, mut repo) = repo_init();
225 let signature = repo.signature().unwrap();
226
227 let p = Path::new(repo.workdir().unwrap()).join("file_b.txt");
228 println!("using path {:?}", p);
229
230 fs::write(&p, "data".as_bytes()).unwrap();
231
232 let rel_p = Path::new("file_b.txt");
233 assert!(repo.status_file(&rel_p).unwrap() == Status::WT_NEW);
234
235 repo.stash_save(&signature, "msg1", Some(StashFlags::INCLUDE_UNTRACKED))
236 .unwrap();
237
238 assert!(repo.status_file(&rel_p).is_err());
239
240 let mut count = 0;
241 repo.stash_foreach(|index, name, _oid| {
242 count += 1;
243 assert!(index == 0);
244 assert!(name == "On main: msg1");
245 true
246 })
247 .unwrap();
248
249 assert!(count == 1);
250 next(&mut repo);
251 }
252
253 fn count_stash(repo: &mut Repository) -> usize {
254 let mut count = 0;
255 repo.stash_foreach(|_, _, _| {
256 count += 1;
257 true
258 })
259 .unwrap();
260 count
261 }
262
263 #[test]
264 fn smoke_stash_save_drop() {
265 make_stash(|repo| {
266 repo.stash_drop(0).unwrap();
267 assert!(count_stash(repo) == 0)
268 })
269 }
270
271 #[test]
272 fn smoke_stash_save_pop() {
273 make_stash(|repo| {
274 repo.stash_pop(0, None).unwrap();
275 assert!(count_stash(repo) == 0)
276 })
277 }
278
279 #[test]
280 fn smoke_stash_save_apply() {
281 make_stash(|repo| {
282 let mut options = StashApplyOptions::new();
283 options.progress_cb(|progress| {
284 println!("{:?}", progress);
285 true
286 });
287
288 repo.stash_apply(0, Some(&mut options)).unwrap();
289 assert!(count_stash(repo) == 1)
290 })
291 }
292
293 #[test]
294 fn test_stash_save2_msg_none() {
295 let (_td, mut repo) = repo_init();
296 let signature = repo.signature().unwrap();
297
298 let p = Path::new(repo.workdir().unwrap()).join("file_b.txt");
299
300 fs::write(&p, "data".as_bytes()).unwrap();
301
302 repo.stash_save2(&signature, None, Some(StashFlags::INCLUDE_UNTRACKED))
303 .unwrap();
304
305 let mut stash_name = String::new();
306 repo.stash_foreach(|index, name, _oid| {
307 assert_eq!(index, 0);
308 stash_name = name.to_string();
309 true
310 })
311 .unwrap();
312
313 assert!(stash_name.starts_with("WIP on main:"));
314 }
315
316 fn create_file(r: &Repository, name: &str, data: &str) -> PathBuf {
317 let p = Path::new(r.workdir().unwrap()).join(name);
318 fs::write(&p, data).unwrap();
319 p
320 }
321
322 #[test]
323 fn test_stash_save_ext() {
324 let (_td, mut repo) = repo_init();
325 let signature = repo.signature().unwrap();
326
327 create_file(&repo, "file_a", "foo");
328 create_file(&repo, "file_b", "foo");
329
330 let mut index = repo.index().unwrap();
331 index
332 .add_all(["*"].iter(), IndexAddOption::DEFAULT, None)
333 .unwrap();
334 index.write().unwrap();
335
336 assert_eq!(repo.statuses(None).unwrap().len(), 2);
337
338 let mut opt = StashSaveOptions::new(signature);
339 opt.pathspec("file_a");
340 repo.stash_save_ext(Some(&mut opt)).unwrap();
341
342 assert_eq!(repo.statuses(None).unwrap().len(), 0);
343
344 repo.stash_pop(0, None).unwrap();
345
346 assert_eq!(repo.statuses(None).unwrap().len(), 1);
347 }
348}