git2/
stash.rs

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
8/// Stash application options structure
9pub 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    /// Creates a default
20    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    /// Customize optional `flags` field
42    pub fn flags(&mut self, flags: Option<StashFlags>) -> &mut Self {
43        self.flags = flags;
44        self
45    }
46
47    /// Add to the array of paths patterns to build the stash.
48    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    /// Acquire a pointer to the underlying raw options.
56    ///
57    /// This function is unsafe as the pointer is only valid so long as this
58    /// structure is not moved, modified, or used elsewhere.
59    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
70/// Stash application progress notification function.
71///
72/// Return `true` to continue processing, or `false` to
73/// abort the stash application.
74// FIXME: This probably should have been pub(crate) since it is not used anywhere.
75pub type StashApplyProgressCb<'a> = dyn FnMut(StashApplyProgress) -> bool + 'a;
76
77/// This is a callback function you can provide to iterate over all the
78/// stashed states that will be invoked per entry.
79// FIXME: This probably should have been pub(crate) since it is not used anywhere.
80pub type StashCb<'a> = dyn FnMut(usize, &str, &Oid) -> bool + 'a;
81
82/// Stash application options structure
83pub 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    /// Creates a default set of merge options.
97    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    /// Set stash application flag to GIT_STASH_APPLY_REINSTATE_INDEX
111    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    /// Options to use when writing files to the working directory
117    pub fn checkout_options(&mut self, opts: CheckoutBuilder<'cb>) -> &mut StashApplyOptions<'cb> {
118        self.checkout_options = Some(opts);
119        self
120    }
121
122    /// Optional callback to notify the consumer of application progress.
123    ///
124    /// Return `true` to continue processing, or `false` to
125    /// abort the stash application.
126    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    /// Pointer to a raw git_stash_apply_options
137    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}