blob: d27f61ac3632010d1c26a41669b365ec9b23038e [file] [log] [blame] [edit]
use std::marker;
use std::mem;
use std::os::raw::c_int;
use std::path::Path;
use std::ptr;
use std::str;
use crate::util::{self, Binding};
use crate::{build::CheckoutBuilder, SubmoduleIgnore, SubmoduleUpdate};
use crate::{raw, Error, FetchOptions, Oid, Repository};
/// A structure to represent a git [submodule][1]
///
/// [1]: http://git-scm.com/book/en/Git-Tools-Submodules
pub struct Submodule<'repo> {
raw: *mut raw::git_submodule,
_marker: marker::PhantomData<&'repo Repository>,
}
impl<'repo> Submodule<'repo> {
/// Get the submodule's branch.
///
/// Returns `None` if the branch is not valid utf-8 or if the branch is not
/// yet available.
pub fn branch(&self) -> Option<&str> {
self.branch_bytes().and_then(|s| str::from_utf8(s).ok())
}
/// Get the branch for the submodule.
///
/// Returns `None` if the branch is not yet available.
pub fn branch_bytes(&self) -> Option<&[u8]> {
unsafe { crate::opt_bytes(self, raw::git_submodule_branch(self.raw)) }
}
/// Perform the clone step for a newly created submodule.
///
/// This performs the necessary `git_clone` to setup a newly-created submodule.
pub fn clone(
&mut self,
opts: Option<&mut SubmoduleUpdateOptions<'_>>,
) -> Result<Repository, Error> {
unsafe {
let raw_opts = opts.map(|o| o.raw());
let mut raw_repo = ptr::null_mut();
try_call!(raw::git_submodule_clone(
&mut raw_repo,
self.raw,
raw_opts.as_ref()
));
Ok(Binding::from_raw(raw_repo))
}
}
/// Get the submodule's url.
///
/// Returns `None` if the url is not valid utf-8 or if the URL isn't present
pub fn url(&self) -> Option<&str> {
self.opt_url_bytes().and_then(|b| str::from_utf8(b).ok())
}
/// Get the url for the submodule.
#[doc(hidden)]
#[deprecated(note = "renamed to `opt_url_bytes`")]
pub fn url_bytes(&self) -> &[u8] {
self.opt_url_bytes().unwrap()
}
/// Get the url for the submodule.
///
/// Returns `None` if the URL isn't present
// TODO: delete this method and fix the signature of `url_bytes` on next
// major version bump
pub fn opt_url_bytes(&self) -> Option<&[u8]> {
unsafe { crate::opt_bytes(self, raw::git_submodule_url(self.raw)) }
}
/// Get the submodule's name.
///
/// Returns `None` if the name is not valid utf-8
pub fn name(&self) -> Option<&str> {
str::from_utf8(self.name_bytes()).ok()
}
/// Get the name for the submodule.
pub fn name_bytes(&self) -> &[u8] {
unsafe { crate::opt_bytes(self, raw::git_submodule_name(self.raw)).unwrap() }
}
/// Get the path for the submodule.
pub fn path(&self) -> &Path {
util::bytes2path(unsafe {
crate::opt_bytes(self, raw::git_submodule_path(self.raw)).unwrap()
})
}
/// Get the OID for the submodule in the current HEAD tree.
pub fn head_id(&self) -> Option<Oid> {
unsafe { Binding::from_raw_opt(raw::git_submodule_head_id(self.raw)) }
}
/// Get the OID for the submodule in the index.
pub fn index_id(&self) -> Option<Oid> {
unsafe { Binding::from_raw_opt(raw::git_submodule_index_id(self.raw)) }
}
/// Get the OID for the submodule in the current working directory.
///
/// This returns the OID that corresponds to looking up 'HEAD' in the
/// checked out submodule. If there are pending changes in the index or
/// anything else, this won't notice that.
pub fn workdir_id(&self) -> Option<Oid> {
unsafe { Binding::from_raw_opt(raw::git_submodule_wd_id(self.raw)) }
}
/// Get the ignore rule that will be used for the submodule.
pub fn ignore_rule(&self) -> SubmoduleIgnore {
SubmoduleIgnore::from_raw(unsafe { raw::git_submodule_ignore(self.raw) })
}
/// Get the update rule that will be used for the submodule.
pub fn update_strategy(&self) -> SubmoduleUpdate {
SubmoduleUpdate::from_raw(unsafe { raw::git_submodule_update_strategy(self.raw) })
}
/// Copy submodule info into ".git/config" file.
///
/// Just like "git submodule init", this copies information about the
/// submodule into ".git/config". You can use the accessor functions above
/// to alter the in-memory git_submodule object and control what is written
/// to the config, overriding what is in .gitmodules.
///
/// By default, existing entries will not be overwritten, but passing `true`
/// for `overwrite` forces them to be updated.
pub fn init(&mut self, overwrite: bool) -> Result<(), Error> {
unsafe {
try_call!(raw::git_submodule_init(self.raw, overwrite));
}
Ok(())
}
/// Open the repository for a submodule.
///
/// This will only work if the submodule is checked out into the working
/// directory.
pub fn open(&self) -> Result<Repository, Error> {
let mut raw = ptr::null_mut();
unsafe {
try_call!(raw::git_submodule_open(&mut raw, self.raw));
Ok(Binding::from_raw(raw))
}
}
/// Reread submodule info from config, index, and HEAD.
///
/// Call this to reread cached submodule information for this submodule if
/// you have reason to believe that it has changed.
///
/// If `force` is `true`, then data will be reloaded even if it doesn't seem
/// out of date
pub fn reload(&mut self, force: bool) -> Result<(), Error> {
unsafe {
try_call!(raw::git_submodule_reload(self.raw, force));
}
Ok(())
}
/// Copy submodule remote info into submodule repo.
///
/// This copies the information about the submodules URL into the checked
/// out submodule config, acting like "git submodule sync". This is useful
/// if you have altered the URL for the submodule (or it has been altered
/// by a fetch of upstream changes) and you need to update your local repo.
pub fn sync(&mut self) -> Result<(), Error> {
unsafe {
try_call!(raw::git_submodule_sync(self.raw));
}
Ok(())
}
/// Add current submodule HEAD commit to index of superproject.
///
/// If `write_index` is true, then the index file will be immediately
/// written. Otherwise you must explicitly call `write()` on an `Index`
/// later on.
pub fn add_to_index(&mut self, write_index: bool) -> Result<(), Error> {
unsafe {
try_call!(raw::git_submodule_add_to_index(self.raw, write_index));
}
Ok(())
}
/// Resolve the setup of a new git submodule.
///
/// This should be called on a submodule once you have called add setup and
/// done the clone of the submodule. This adds the .gitmodules file and the
/// newly cloned submodule to the index to be ready to be committed (but
/// doesn't actually do the commit).
pub fn add_finalize(&mut self) -> Result<(), Error> {
unsafe {
try_call!(raw::git_submodule_add_finalize(self.raw));
}
Ok(())
}
/// Update submodule.
///
/// This will clone a missing submodule and check out the subrepository to
/// the commit specified in the index of the containing repository. If
/// the submodule repository doesn't contain the target commit, then the
/// submodule is fetched using the fetch options supplied in `opts`.
///
/// `init` indicates if the submodule should be initialized first if it has
/// not been initialized yet.
pub fn update(
&mut self,
init: bool,
opts: Option<&mut SubmoduleUpdateOptions<'_>>,
) -> Result<(), Error> {
unsafe {
let mut raw_opts = opts.map(|o| o.raw());
try_call!(raw::git_submodule_update(
self.raw,
init as c_int,
raw_opts.as_mut().map_or(ptr::null_mut(), |o| o)
));
}
Ok(())
}
}
impl<'repo> Binding for Submodule<'repo> {
type Raw = *mut raw::git_submodule;
unsafe fn from_raw(raw: *mut raw::git_submodule) -> Submodule<'repo> {
Submodule {
raw,
_marker: marker::PhantomData,
}
}
fn raw(&self) -> *mut raw::git_submodule {
self.raw
}
}
impl<'repo> Drop for Submodule<'repo> {
fn drop(&mut self) {
unsafe { raw::git_submodule_free(self.raw) }
}
}
/// Options to update a submodule.
pub struct SubmoduleUpdateOptions<'cb> {
checkout_builder: CheckoutBuilder<'cb>,
fetch_opts: FetchOptions<'cb>,
allow_fetch: bool,
}
impl<'cb> SubmoduleUpdateOptions<'cb> {
/// Return default options.
pub fn new() -> Self {
SubmoduleUpdateOptions {
checkout_builder: CheckoutBuilder::new(),
fetch_opts: FetchOptions::new(),
allow_fetch: true,
}
}
unsafe fn raw(&mut self) -> raw::git_submodule_update_options {
let mut checkout_opts: raw::git_checkout_options = mem::zeroed();
let init_res =
raw::git_checkout_init_options(&mut checkout_opts, raw::GIT_CHECKOUT_OPTIONS_VERSION);
assert_eq!(0, init_res);
self.checkout_builder.configure(&mut checkout_opts);
let opts = raw::git_submodule_update_options {
version: raw::GIT_SUBMODULE_UPDATE_OPTIONS_VERSION,
checkout_opts,
fetch_opts: self.fetch_opts.raw(),
allow_fetch: self.allow_fetch as c_int,
};
opts
}
/// Set checkout options.
pub fn checkout(&mut self, opts: CheckoutBuilder<'cb>) -> &mut Self {
self.checkout_builder = opts;
self
}
/// Set fetch options and allow fetching.
pub fn fetch(&mut self, opts: FetchOptions<'cb>) -> &mut Self {
self.fetch_opts = opts;
self.allow_fetch = true;
self
}
/// Allow or disallow fetching.
pub fn allow_fetch(&mut self, b: bool) -> &mut Self {
self.allow_fetch = b;
self
}
}
impl<'cb> Default for SubmoduleUpdateOptions<'cb> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::Path;
use tempfile::TempDir;
use url::Url;
use crate::Repository;
use crate::SubmoduleUpdateOptions;
#[test]
fn smoke() {
let td = TempDir::new().unwrap();
let repo = Repository::init(td.path()).unwrap();
let mut s1 = repo
.submodule("/path/to/nowhere", Path::new("foo"), true)
.unwrap();
s1.init(false).unwrap();
s1.sync().unwrap();
let s2 = repo
.submodule("/path/to/nowhere", Path::new("bar"), true)
.unwrap();
drop((s1, s2));
let mut submodules = repo.submodules().unwrap();
assert_eq!(submodules.len(), 2);
let mut s = submodules.remove(0);
assert_eq!(s.name(), Some("bar"));
assert_eq!(s.url(), Some("/path/to/nowhere"));
assert_eq!(s.branch(), None);
assert!(s.head_id().is_none());
assert!(s.index_id().is_none());
assert!(s.workdir_id().is_none());
repo.find_submodule("bar").unwrap();
s.open().unwrap();
assert!(s.path() == Path::new("bar"));
s.reload(true).unwrap();
}
#[test]
fn add_a_submodule() {
let (_td, repo1) = crate::test::repo_init();
let (td, repo2) = crate::test::repo_init();
let url = Url::from_file_path(&repo1.workdir().unwrap()).unwrap();
let mut s = repo2
.submodule(&url.to_string(), Path::new("bar"), true)
.unwrap();
t!(fs::remove_dir_all(td.path().join("bar")));
t!(Repository::clone(&url.to_string(), td.path().join("bar")));
t!(s.add_to_index(false));
t!(s.add_finalize());
}
#[test]
fn update_submodule() {
// -----------------------------------
// Same as `add_a_submodule()`
let (_td, repo1) = crate::test::repo_init();
let (td, repo2) = crate::test::repo_init();
let url = Url::from_file_path(&repo1.workdir().unwrap()).unwrap();
let mut s = repo2
.submodule(&url.to_string(), Path::new("bar"), true)
.unwrap();
t!(fs::remove_dir_all(td.path().join("bar")));
t!(Repository::clone(&url.to_string(), td.path().join("bar")));
t!(s.add_to_index(false));
t!(s.add_finalize());
// -----------------------------------
// Attempt to update submodule
let submodules = t!(repo1.submodules());
for mut submodule in submodules {
let mut submodule_options = SubmoduleUpdateOptions::new();
let init = true;
let opts = Some(&mut submodule_options);
t!(submodule.update(init, opts));
}
}
#[test]
fn clone_submodule() {
// -----------------------------------
// Same as `add_a_submodule()`
let (_td, repo1) = crate::test::repo_init();
let (_td, repo2) = crate::test::repo_init();
let (_td, parent) = crate::test::repo_init();
let url1 = Url::from_file_path(&repo1.workdir().unwrap()).unwrap();
let url3 = Url::from_file_path(&repo2.workdir().unwrap()).unwrap();
let mut s1 = parent
.submodule(&url1.to_string(), Path::new("bar"), true)
.unwrap();
let mut s2 = parent
.submodule(&url3.to_string(), Path::new("bar2"), true)
.unwrap();
// -----------------------------------
t!(s1.clone(Some(&mut SubmoduleUpdateOptions::default())));
t!(s2.clone(None));
}
}