diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index 83b14fe146..5bf894d07e 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -1814,6 +1814,15 @@ git_enum! { } } +git_enum! { + pub enum git_reference_format_t { + GIT_REFERENCE_FORMAT_NORMAL = 0, + GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL = 1 << 0, + GIT_REFERENCE_FORMAT_REFSPEC_PATTERN = 1 << 1, + GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND = 1 << 2, + } +} + extern "C" { // threads pub fn git_libgit2_init() -> c_int; @@ -2278,6 +2287,12 @@ extern "C" { ) -> c_int; pub fn git_reference_has_log(repo: *mut git_repository, name: *const c_char) -> c_int; pub fn git_reference_ensure_log(repo: *mut git_repository, name: *const c_char) -> c_int; + pub fn git_reference_normalize_name( + buffer_out: *mut c_char, + buffer_size: size_t, + name: *const c_char, + flags: u32, + ) -> c_int; // stash pub fn git_stash_save( diff --git a/src/lib.rs b/src/lib.rs index 1ce760ca7a..14cc9b7d3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1403,6 +1403,40 @@ impl DiffFlags { is_bit_set!(exists, DiffFlags::EXISTS); } +bitflags! { + /// Options for [`Reference::normalize_name`]. + pub struct ReferenceFormat: u32 { + /// No particular normalization. + const NORMAL = raw::GIT_REFERENCE_FORMAT_NORMAL as u32; + /// Constrol whether one-level refname are accepted (i.e., refnames that + /// do not contain multiple `/`-separated components). Those are + /// expected to be written only using uppercase letters and underscore + /// (e.g. `HEAD`, `FETCH_HEAD`). + const ALLOW_ONELEVEL = raw::GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL as u32; + /// Interpret the provided name as a reference pattern for a refspec (as + /// used with remote repositories). If this option is enabled, the name + /// is allowed to contain a single `*` in place of a full pathname + /// components (e.g., `foo/*/bar` but not `foo/bar*`). + const REFSPEC_PATTERN = raw::GIT_REFERENCE_FORMAT_REFSPEC_PATTERN as u32; + /// Interpret the name as part of a refspec in shorthand form so the + /// `ALLOW_ONELEVEL` naming rules aren't enforced and `master` becomes a + /// valid name. + const REFSPEC_SHORTHAND = raw::GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND as u32; + } +} + +impl ReferenceFormat { + is_bit_set!(is_allow_onelevel, ReferenceFormat::ALLOW_ONELEVEL); + is_bit_set!(is_refspec_pattern, ReferenceFormat::REFSPEC_PATTERN); + is_bit_set!(is_refspec_shorthand, ReferenceFormat::REFSPEC_SHORTHAND); +} + +impl Default for ReferenceFormat { + fn default() -> Self { + ReferenceFormat::NORMAL + } +} + #[cfg(test)] mod tests { use super::ObjectType; diff --git a/src/reference.rs b/src/reference.rs index 7955fcfe78..09f4f88671 100644 --- a/src/reference.rs +++ b/src/reference.rs @@ -8,9 +8,14 @@ use std::str; use crate::object::CastOrPanic; use crate::util::{c_cmp_to_ordering, Binding}; use crate::{ - raw, Blob, Commit, Error, Object, ObjectType, Oid, ReferenceType, Repository, Tag, Tree, + raw, Blob, Commit, Error, Object, ObjectType, Oid, ReferenceFormat, ReferenceType, Repository, + Tag, Tree, }; +// Not in the public header files (yet?), but a hard limit used by libgit2 +// internally +const GIT_REFNAME_MAX: usize = 1024; + struct Refdb<'repo>(&'repo Repository); /// A structure to represent a git [reference][1]. @@ -34,12 +39,120 @@ pub struct ReferenceNames<'repo, 'references> { impl<'repo> Reference<'repo> { /// Ensure the reference name is well-formed. + /// + /// Validation is performed as if [`ReferenceFormat::ALLOW_ONELEVEL`] + /// was given to [`Reference::normalize_name`]. No normalization is + /// performed, however. + /// + /// ```rust + /// use git2::Reference; + /// + /// assert!(Reference::is_valid_name("HEAD")); + /// assert!(Reference::is_valid_name("refs/heads/master")); + /// + /// // But: + /// assert!(!Reference::is_valid_name("master")); + /// assert!(!Reference::is_valid_name("refs/heads/*")); + /// assert!(!Reference::is_valid_name("foo//bar")); + /// ``` + /// + /// [`ReferenceFormat::ALLOW_ONELEVEL`]: + /// struct.ReferenceFormat#associatedconstant.ALLOW_ONELEVEL + /// [`Reference::normalize_name`]: struct.Reference#method.normalize_name pub fn is_valid_name(refname: &str) -> bool { crate::init(); let refname = CString::new(refname).unwrap(); unsafe { raw::git_reference_is_valid_name(refname.as_ptr()) == 1 } } + /// Normalize reference name and check validity. + /// + /// This will normalize the reference name by collapsing runs of adjacent + /// slashes between name components into a single slash. It also validates + /// the name according to the following rules: + /// + /// 1. If [`ReferenceFormat::ALLOW_ONELEVEL`] is given, the name may + /// contain only capital letters and underscores, and must begin and end + /// with a letter. (e.g. "HEAD", "ORIG_HEAD"). + /// 2. The flag [`ReferenceFormat::REFSPEC_SHORTHAND`] has an effect + /// only when combined with [`ReferenceFormat::ALLOW_ONELEVEL`]. If + /// it is given, "shorthand" branch names (i.e. those not prefixed by + /// `refs/`, but consisting of a single word without `/` separators) + /// become valid. For example, "master" would be accepted. + /// 3. If [`ReferenceFormat::REFSPEC_PATTERN`] is given, the name may + /// contain a single `*` in place of a full pathname component (e.g. + /// `foo/*/bar`, `foo/bar*`). + /// 4. Names prefixed with "refs/" can be almost anything. You must avoid + /// the characters '~', '^', ':', '\\', '?', '[', and '*', and the + /// sequences ".." and "@{" which have special meaning to revparse. + /// + /// If the reference passes validation, it is returned in normalized form, + /// otherwise an [`Error`] with [`ErrorCode::InvalidSpec`] is returned. + /// + /// ```rust + /// use git2::{Reference, ReferenceFormat}; + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "foo//bar", + /// ReferenceFormat::NORMAL + /// ) + /// .unwrap(), + /// "foo/bar".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "HEAD", + /// ReferenceFormat::ALLOW_ONELEVEL + /// ) + /// .unwrap(), + /// "HEAD".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "refs/heads/*", + /// ReferenceFormat::REFSPEC_PATTERN + /// ) + /// .unwrap(), + /// "refs/heads/*".to_owned() + /// ); + /// + /// assert_eq!( + /// Reference::normalize_name( + /// "master", + /// ReferenceFormat::ALLOW_ONELEVEL | ReferenceFormat::REFSPEC_SHORTHAND + /// ) + /// .unwrap(), + /// "master".to_owned() + /// ); + /// ``` + /// + /// [`ReferenceFormat::ALLOW_ONELEVEL`]: + /// struct.ReferenceFormat#associatedconstant.ALLOW_ONELEVEL + /// [`ReferenceFormat::REFSPEC_SHORTHAND`]: + /// struct.ReferenceFormat#associatedconstant.REFSPEC_SHORTHAND + /// [`ReferenceFormat::REFSPEC_PATTERN`]: + /// struct.ReferenceFormat#associatedconstant.REFSPEC_PATTERN + /// [`Error`]: struct.Error + /// [`ErrorCode::InvalidSpec`]: enum.ErrorCode#variant.InvalidSpec + pub fn normalize_name(refname: &str, flags: ReferenceFormat) -> Result { + crate::init(); + let mut dst = [0u8; GIT_REFNAME_MAX]; + let refname = CString::new(refname)?; + unsafe { + try_call!(raw::git_reference_normalize_name( + dst.as_mut_ptr() as *mut libc::c_char, + dst.len() as libc::size_t, + refname, + flags.bits() + )); + let s = &dst[..dst.iter().position(|&a| a == 0).unwrap()]; + Ok(str::from_utf8(s).unwrap().to_owned()) + } + } + /// Get access to the underlying raw pointer. pub fn raw(&self) -> *mut raw::git_reference { self.raw