miss-0: src/Data/Git/RefName.hs
{-# LANGUAGE OverloadedStrings #-}
{-|
Description: Safe reference and remote names.
Git constrains the names of references (branches, tags, etc.) to a collection of 10-or-so rules (see
@git help check-ref-format@ for details). This module provides checked wrappers for refnames and
remote names.
-}
module Data.Git.RefName
(
-- * Reference Names
RefName
, getRefName
, refName
, checkRefFormat
-- * Remote Names
, RemoteName
, getRemoteName
, remoteName
, checkRemoteName
) where
import qualified Data.ByteString as B
import Data.String
import System.Posix.FilePath
-- | Safe reference names. Construct with 'refName'.
newtype RefName = RefName
{
getRefName :: RawFilePath -- ^ The 'RefName's underlying 'RawFilePath'
} deriving (Eq, Ord, Show)
instance IsString RefName where
fromString rn = maybe (error $ "invalid refname: " ++ rn)
id
(refName . fromString $ rn)
-- | Try to make a 'RefName', given that 'checkRefFormat' allows it.
refName :: RawFilePath -> Maybe RefName
refName name | checkRefFormat True False name = Just (RefName name)
| otherwise = Nothing
-- | Check a potential refname against the rules for well formed refnames according to @git help
-- check-ref-format@
checkRefFormat :: Bool -- ^ Allow refnames with no @/@s, as @--allow-onelevel@
-> Bool -- ^ Allow one and only one asterisk in a ref, as @--refspec-pattern@
-> RawFilePath
-> Bool
checkRefFormat allowOneLevel refSpecPattern name =
not (B.null name)
&& not (any (\c -> "." `B.isPrefixOf` c -- 1. They can include slash / for hierarchical
|| ".lock" `B.isSuffixOf` c) -- (directory) grouping, but no slash-separated component
(splitDirectories name)) -- can begin with a dot . or end with the sequence
-- .lock.
&& (allowOneLevel -- 2. They must contain at least one /. This enforces the
|| ("/" `B.isInfixOf` name)) -- presence of a category like heads/, tags/ etc. but the
-- actual names are not restricted. If the --allow-onelevel
-- option is used, this rule is waived.
&& not (".." `B.isInfixOf` name) -- 3. They cannot have two consecutive dots .. anywhere.
&& not (B.any (\c -> c < 040 -- 4. They cannot have ASCII control characters (i.e. bytes
|| c == 0o177 -- whose values are lower than \040, or \177 DEL), space,
|| c `B.elem` " \t\n\r\f\v~^:") name) -- tilde ~, caret ^, or colon : anywhere.
&& not (B.any (`B.elem` "?[") name -- 5. They cannot have question-mark ?, asterisk *, or open
|| (refSpecPattern -- bracket [ anywhere. See the --refspec-pattern option
&& B.count 0o52 name > 1) -- below for an exception to this rule. (0o52 == '*')
|| (not refSpecPattern
&& 0o52 `B.elem` name))
&& not ("/" `B.isSuffixOf` name -- 6. They cannot begin or end with a slash / or contain
|| "/" `B.isPrefixOf` name -- multiple consecutive slashes (see the --normalize option
|| "//" `B.isInfixOf` name) -- below for an exception to this rule)
&& not ("." `B.isSuffixOf` name) -- 7. They cannot end with a dot ..
&& not ("@{" `B.isInfixOf` name) -- 8. They cannot contain a sequence @{.
&& "@" /= name -- 9. They cannot be the single character @.
&& not ("\\" `B.isInfixOf` name) -- 10. They cannot contain a \.
-- | Safe remote names. Construct with 'remoteName'.
newtype RemoteName = RemoteName
{
getRemoteName :: B.ByteString -- ^ The 'RemoteName's underlying 'RawFilePath'
} deriving (Eq, Ord, Show)
instance IsString RemoteName where
fromString rn = maybe (error $ "invalid remote name: " ++ rn)
id
(remoteName . fromString $ rn)
-- | Ensure the name is contains no @/@, and is none of: @""@, @"."@, per
-- https://github.com/git/git/blob/1f66975deb8402131fbf7c14330d0c7cdebaeaa2/remote.c#L644.
checkRemoteName :: B.ByteString -> Bool
checkRemoteName b = b /= "" && b /= "." && b /= ".." && B.notElem 0o57 b
-- | Try to make a 'RemoteName', ensuring it's valid according to 'checkRemoteName'.
remoteName :: RawFilePath -> Maybe RemoteName
remoteName name | checkRemoteName name = Just (RemoteName name)
| otherwise = Nothing