Skip to content

Support partial stashing #930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions libgit2-sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1731,6 +1731,7 @@ git_enum! {
GIT_STASH_KEEP_INDEX = 1 << 0,
GIT_STASH_INCLUDE_UNTRACKED = 1 << 1,
GIT_STASH_INCLUDE_IGNORED = 1 << 2,
GIT_STASH_KEEP_ALL = 1 << 3,
}
}

Expand All @@ -1754,6 +1755,17 @@ git_enum! {
}
}

#[repr(C)]
pub struct git_stash_save_options {
pub version: c_uint,
pub flags: u32,
pub stasher: *const git_signature,
pub message: *const c_char,
pub paths: git_strarray,
}

pub const GIT_STASH_SAVE_OPTIONS_VERSION: c_uint = 1;

#[repr(C)]
pub struct git_stash_apply_options {
pub version: c_uint,
Expand Down Expand Up @@ -2534,6 +2546,15 @@ extern "C" {
flags: c_uint,
) -> c_int;

pub fn git_stash_save_options_init(opts: *mut git_stash_save_options, version: c_uint)
-> c_int;

pub fn git_stash_save_with_opts(
out: *mut git_oid,
repo: *mut git_repository,
options: *const git_stash_save_options,
) -> c_int;

pub fn git_stash_apply_init_options(
opts: *mut git_stash_apply_options,
version: c_uint,
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,8 @@ bitflags! {
/// All ignored files are also stashed and then cleaned up from
/// the working directory
const INCLUDE_IGNORED = raw::GIT_STASH_INCLUDE_IGNORED as u32;
/// All changes in the index and working directory are left intact
const KEEP_ALL = raw::GIT_STASH_KEEP_ALL as u32;
}
}

Expand Down
21 changes: 20 additions & 1 deletion src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::diff::{
binary_cb_c, file_cb_c, hunk_cb_c, line_cb_c, BinaryCb, DiffCallbacks, FileCb, HunkCb, LineCb,
};
use crate::oid_array::OidArray;
use crate::stash::{stash_cb, StashApplyOptions, StashCbData};
use crate::stash::{stash_cb, StashApplyOptions, StashCbData, StashSaveOptions};
use crate::string_array::StringArray;
use crate::tagforeach::{tag_foreach_cb, TagForeachCB, TagForeachData};
use crate::util::{self, path_to_repo_path, Binding};
Expand Down Expand Up @@ -2844,6 +2844,25 @@ impl Repository {
}
}

/// Like `stash_save` but with more options like selective statshing via path patterns.
pub fn stash_save_ext(
&mut self,
opts: Option<&mut StashSaveOptions<'_>>,
) -> Result<Oid, Error> {
unsafe {
let mut raw_oid = raw::git_oid {
id: [0; raw::GIT_OID_RAWSZ],
};
let opts = opts.map(|opts| opts.raw());
try_call!(raw::git_stash_save_with_opts(
&mut raw_oid,
self.raw(),
opts
));
Ok(Binding::from_raw(&raw_oid as *const _))
}
}

/// Apply a single stashed state from the stash list.
pub fn stash_apply(
&mut self,
Expand Down
120 changes: 105 additions & 15 deletions src/stash.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,73 @@
use crate::build::CheckoutBuilder;
use crate::util::Binding;
use crate::{panic, raw, Oid, StashApplyProgress};
use crate::util::{self, Binding};
use crate::{panic, raw, IntoCString, Oid, Signature, StashApplyProgress, StashFlags};
use libc::{c_char, c_int, c_void, size_t};
use std::ffi::CStr;
use std::ffi::{c_uint, CStr, CString};
use std::mem;

#[allow(unused)]
/// Stash application options structure
pub struct StashSaveOptions<'a> {
message: Option<CString>,
flags: Option<StashFlags>,
stasher: Signature<'a>,
pathspec: Vec<CString>,
pathspec_ptrs: Vec<*const c_char>,
raw_opts: raw::git_stash_save_options,
}

impl<'a> StashSaveOptions<'a> {
/// Creates a default
pub fn new(stasher: Signature<'a>) -> Self {
let mut opts = Self {
message: None,
flags: None,
stasher,
pathspec: Vec::new(),
pathspec_ptrs: Vec::new(),
raw_opts: unsafe { mem::zeroed() },
};
assert_eq!(
unsafe {
raw::git_stash_save_options_init(
&mut opts.raw_opts,
raw::GIT_STASH_SAVE_OPTIONS_VERSION,
)
},
0
);
opts
}

/// Customize optional `flags` field
pub fn flags(&mut self, flags: Option<StashFlags>) -> &mut Self {
self.flags = flags;
self
}

/// Add to the array of paths patterns to build the stash.
pub fn pathspec<T: IntoCString>(&mut self, pathspec: T) -> &mut Self {
let s = util::cstring_to_repo_path(pathspec).unwrap();
self.pathspec_ptrs.push(s.as_ptr());
self.pathspec.push(s);
self
}

/// Acquire a pointer to the underlying raw options.
///
/// This function is unsafe as the pointer is only valid so long as this
/// structure is not moved, modified, or used elsewhere.
pub unsafe fn raw(&mut self) -> *const raw::git_stash_save_options {
self.raw_opts.flags = self.flags.unwrap_or_else(StashFlags::empty).bits as c_uint;
self.raw_opts.message = crate::call::convert(&self.message);
self.raw_opts.paths.count = self.pathspec_ptrs.len() as size_t;
self.raw_opts.paths.strings = self.pathspec_ptrs.as_ptr() as *mut _;
self.raw_opts.stasher = self.stasher.raw();

&self.raw_opts as *const _
}
}

/// Stash application progress notification function.
///
/// Return `true` to continue processing, or `false` to
Expand Down Expand Up @@ -151,12 +214,11 @@ extern "C" fn stash_apply_progress_cb(

#[cfg(test)]
mod tests {
use crate::stash::StashApplyOptions;
use crate::stash::{StashApplyOptions, StashSaveOptions};
use crate::test::repo_init;
use crate::{Repository, StashFlags, Status};
use crate::{IndexAddOption, Repository, StashFlags, Status};
use std::fs;
use std::io::Write;
use std::path::Path;
use std::path::{Path, PathBuf};

fn make_stash<C>(next: C)
where
Expand All @@ -167,10 +229,8 @@ mod tests {

let p = Path::new(repo.workdir().unwrap()).join("file_b.txt");
println!("using path {:?}", p);
fs::File::create(&p)
.unwrap()
.write("data".as_bytes())
.unwrap();

fs::write(&p, "data".as_bytes()).unwrap();

let rel_p = Path::new("file_b.txt");
assert!(repo.status_file(&rel_p).unwrap() == Status::WT_NEW);
Expand Down Expand Up @@ -240,10 +300,7 @@ mod tests {

let p = Path::new(repo.workdir().unwrap()).join("file_b.txt");

fs::File::create(&p)
.unwrap()
.write("data".as_bytes())
.unwrap();
fs::write(&p, "data".as_bytes()).unwrap();

repo.stash_save2(&signature, None, Some(StashFlags::INCLUDE_UNTRACKED))
.unwrap();
Expand All @@ -258,4 +315,37 @@ mod tests {

assert!(stash_name.starts_with("WIP on main:"));
}

fn create_file(r: &Repository, name: &str, data: &str) -> PathBuf {
let p = Path::new(r.workdir().unwrap()).join(name);
fs::write(&p, data).unwrap();
p
}

#[test]
fn test_stash_save_ext() {
let (_td, mut repo) = repo_init();
let signature = repo.signature().unwrap();

create_file(&repo, "file_a", "foo");
create_file(&repo, "file_b", "foo");

let mut index = repo.index().unwrap();
index
.add_all(["*"].iter(), IndexAddOption::DEFAULT, None)
.unwrap();
index.write().unwrap();

assert_eq!(repo.statuses(None).unwrap().len(), 2);

let mut opt = StashSaveOptions::new(signature);
opt.pathspec("file_a");
repo.stash_save_ext(Some(&mut opt)).unwrap();

assert_eq!(repo.statuses(None).unwrap().len(), 0);

repo.stash_pop(0, None).unwrap();

assert_eq!(repo.statuses(None).unwrap().len(), 1);
}
}