251 lines
		
	
	
	
		
			7.4 KiB
			
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			251 lines
		
	
	
	
		
			7.4 KiB
			
		
	
	
	
		
			Haskell
		
	
	
	
	
	
{- git ref stuff
 | 
						|
 -
 | 
						|
 - Copyright 2011-2024 Joey Hess <id@joeyh.name>
 | 
						|
 -
 | 
						|
 - Licensed under the GNU AGPL version 3 or higher.
 | 
						|
 -}
 | 
						|
 | 
						|
{-# LANGUAGE OverloadedStrings #-}
 | 
						|
 | 
						|
module Git.Ref where
 | 
						|
 | 
						|
import Common
 | 
						|
import Git
 | 
						|
import Git.Command
 | 
						|
import Git.Sha
 | 
						|
import Git.Types
 | 
						|
import Git.FilePath
 | 
						|
 | 
						|
import Data.Char (chr, ord)
 | 
						|
import qualified Data.ByteString as S
 | 
						|
import qualified Data.ByteString.Char8 as S8
 | 
						|
 | 
						|
headRef :: Ref
 | 
						|
headRef = Ref "HEAD"
 | 
						|
 | 
						|
headFile :: Repo -> FilePath
 | 
						|
headFile r = fromRawFilePath (localGitDir r) </> "HEAD"
 | 
						|
 | 
						|
setHeadRef :: Ref -> Repo -> IO ()
 | 
						|
setHeadRef ref r = S.writeFile (headFile r) ("ref: " <> fromRef' ref)
 | 
						|
 | 
						|
{- Converts a fully qualified git ref into a user-visible string. -}
 | 
						|
describe :: Ref -> String
 | 
						|
describe = fromRef . base
 | 
						|
 | 
						|
{- Often git refs are fully qualified 
 | 
						|
 - (eg refs/heads/master or refs/remotes/origin/master).
 | 
						|
 - Converts such a fully qualified ref into a base ref
 | 
						|
 - (eg: master or origin/master). -}
 | 
						|
base :: Ref -> Ref
 | 
						|
base = removeBase "refs/heads/" . removeBase "refs/remotes/"
 | 
						|
 | 
						|
{- Removes a directory such as "refs/heads/master" from a
 | 
						|
 - fully qualified ref. Any ref not starting with it is left as-is. -}
 | 
						|
removeBase :: String -> Ref -> Ref
 | 
						|
removeBase dir r
 | 
						|
	| prefix `isPrefixOf` rs = Ref $ encodeBS $ drop (length prefix) rs
 | 
						|
	| otherwise = r
 | 
						|
  where
 | 
						|
	rs = fromRef r
 | 
						|
	prefix = case end dir of
 | 
						|
		['/'] -> dir
 | 
						|
		_ -> dir ++ "/"
 | 
						|
 | 
						|
{- Given a directory such as "refs/remotes/origin", and a ref such as
 | 
						|
 - refs/heads/master, yields a version of that ref under the directory,
 | 
						|
 - such as refs/remotes/origin/master. -}
 | 
						|
underBase :: String -> Ref -> Ref
 | 
						|
underBase dir r = Ref $ encodeBS dir <> "/" <> fromRef' (base r)
 | 
						|
 | 
						|
{- Convert a branch such as "master" into a fully qualified ref. -}
 | 
						|
branchRef :: Branch -> Ref
 | 
						|
branchRef = underBase "refs/heads"
 | 
						|
 | 
						|
{- A Ref that can be used to refer to a file in the repository, as staged
 | 
						|
 - in the index.
 | 
						|
 - 
 | 
						|
 - If the input file is located outside the repository, returns Nothing.
 | 
						|
 -}
 | 
						|
fileRef :: RawFilePath -> Repo -> IO (Maybe Ref)
 | 
						|
fileRef f repo = do
 | 
						|
	-- The filename could be absolute, or contain eg "../repo/file",
 | 
						|
	-- neither of which work in a ref, so convert it to a minimal
 | 
						|
	-- relative path.
 | 
						|
	f' <- relPathCwdToFile f
 | 
						|
	return $ if repoPath repo `dirContains` f'
 | 
						|
 		-- Prefixing the file with ./ makes this work even when in a
 | 
						|
		-- subdirectory of a repo. Eg, ./foo in directory bar refers
 | 
						|
		-- to bar/foo, not to foo in the top of the repository.
 | 
						|
		then Just $ Ref $ ":./" <> toInternalGitPath f'
 | 
						|
		else Nothing
 | 
						|
 | 
						|
{- A Ref that can be used to refer to a file in a particular branch. -}
 | 
						|
branchFileRef :: Branch -> RawFilePath -> Ref
 | 
						|
branchFileRef branch f = Ref $ fromRef' branch <> ":" <> toInternalGitPath f
 | 
						|
 | 
						|
{- Converts a Ref to refer to the content of the Ref on a given date. -}
 | 
						|
dateRef :: Ref -> RefDate -> Ref
 | 
						|
dateRef r (RefDate d) = Ref $ fromRef' r <> "@" <> encodeBS d
 | 
						|
 | 
						|
{- A Ref that can be used to refer to a file in the repository as it
 | 
						|
 - appears in a given Ref. 
 | 
						|
 -
 | 
						|
 - If the file path is located outside the repository, returns Nothing.
 | 
						|
 -}
 | 
						|
fileFromRef :: Ref -> RawFilePath -> Repo -> IO (Maybe Ref)
 | 
						|
fileFromRef r f repo = fileRef f repo >>= return . \case
 | 
						|
	Just (Ref fr) -> Just (Ref (fromRef' r <> fr))
 | 
						|
	Nothing -> Nothing
 | 
						|
 | 
						|
{- Checks if a ref exists. Note that it must be fully qualified,
 | 
						|
 - eg refs/heads/master rather than master. -}
 | 
						|
exists :: Ref -> Repo -> IO Bool
 | 
						|
exists ref = runBool
 | 
						|
	[ Param "show-ref"
 | 
						|
	, Param "--verify"
 | 
						|
	, Param "-q"
 | 
						|
	, Param $ fromRef ref
 | 
						|
	]
 | 
						|
 | 
						|
{- The file used to record a ref. (Git also stores some refs in a
 | 
						|
 - packed-refs file.) -}
 | 
						|
file :: Ref -> Repo -> FilePath
 | 
						|
file ref repo = fromRawFilePath (localGitDir repo) </> fromRef ref
 | 
						|
 | 
						|
{- Checks if HEAD exists. It generally will, except for in a repository
 | 
						|
 - that was just created. -}
 | 
						|
headExists :: Repo -> IO Bool
 | 
						|
headExists repo = do
 | 
						|
	ls <- S.split nl <$> pipeReadStrict [Param "show-ref", Param "--head"] repo
 | 
						|
	return $ any (" HEAD" `S.isSuffixOf`) ls
 | 
						|
  where
 | 
						|
	nl = fromIntegral (ord '\n')
 | 
						|
 | 
						|
{- Get the sha of a fully qualified git ref, if it exists. -}
 | 
						|
sha :: Branch -> Repo -> IO (Maybe Sha)
 | 
						|
sha branch repo = process <$> showref repo
 | 
						|
  where
 | 
						|
	showref = pipeReadStrict
 | 
						|
		[ Param "show-ref"
 | 
						|
		, Param "--hash" -- get the hash
 | 
						|
		, Param $ fromRef branch
 | 
						|
		]
 | 
						|
	process s
 | 
						|
		| S.null s = Nothing
 | 
						|
		| otherwise = Just $ Ref $ firstLine' s
 | 
						|
 | 
						|
headSha :: Repo -> IO (Maybe Sha)
 | 
						|
headSha = sha headRef
 | 
						|
 | 
						|
{- List of (shas, branches) matching a given ref or refs. -}
 | 
						|
matching :: [Ref] -> Repo -> IO [(Sha, Branch)]
 | 
						|
matching = matching' []
 | 
						|
 | 
						|
{- Includes HEAD in the output, if asked for it. -}
 | 
						|
matchingWithHEAD :: [Ref] -> Repo -> IO [(Sha, Branch)]
 | 
						|
matchingWithHEAD = matching' [Param "--head"]
 | 
						|
 | 
						|
matching' :: [CommandParam] -> [Ref] -> Repo -> IO [(Sha, Branch)]
 | 
						|
matching' ps rs repo = map gen . S8.lines <$> 
 | 
						|
	pipeReadStrict (Param "show-ref" : ps ++ rps) repo
 | 
						|
  where
 | 
						|
	gen l = let (r, b) = separate' (== fromIntegral (ord ' ')) l
 | 
						|
		in (Ref r, Ref b)
 | 
						|
	rps = map (Param . fromRef) rs
 | 
						|
 | 
						|
{- List of (shas, branches) matching a given ref.
 | 
						|
 - Duplicate shas are filtered out. -}
 | 
						|
matchingUniq :: [Ref] -> Repo -> IO [(Sha, Branch)]
 | 
						|
matchingUniq refs repo = nubBy uniqref <$> matching refs repo
 | 
						|
  where
 | 
						|
	uniqref (a, _) (b, _) = a == b
 | 
						|
 | 
						|
{- List of all refs. -}
 | 
						|
list :: Repo -> IO [(Sha, Ref)]
 | 
						|
list = matching' [] []
 | 
						|
 | 
						|
{- Lists refs using for-each-ref.  -}
 | 
						|
forEachRef :: [CommandParam] -> Repo -> IO [(Sha, Branch)]
 | 
						|
forEachRef ps repo = map gen . S8.lines <$>
 | 
						|
	pipeReadStrict (Param "for-each-ref" : ps ++ [format]) repo
 | 
						|
  where
 | 
						|
	format = Param "--format=%(objectname) %(refname)"
 | 
						|
	gen l = let (r, b) = separate' (== fromIntegral (ord ' ')) l
 | 
						|
		in (Ref r, Ref b)
 | 
						|
 | 
						|
{- Deletes a ref when it contains the specified sha. 
 | 
						|
 - This can delete refs that are not branches, which
 | 
						|
 - git branch --delete refuses to delete. -}
 | 
						|
delete :: Sha -> Ref -> Repo -> IO ()
 | 
						|
delete oldvalue ref = run
 | 
						|
	[ Param "update-ref"
 | 
						|
	, Param "-d"
 | 
						|
	, Param $ fromRef ref
 | 
						|
	, Param $ fromRef oldvalue
 | 
						|
	]
 | 
						|
 | 
						|
{- Deletes a ref no matter what it contains. -}
 | 
						|
delete' :: Ref -> Repo -> IO ()
 | 
						|
delete' ref = run
 | 
						|
	[ Param "update-ref"
 | 
						|
	, Param "-d"
 | 
						|
	, Param $ fromRef ref
 | 
						|
	]
 | 
						|
 | 
						|
{- Gets the sha of the tree a ref uses. 
 | 
						|
 -
 | 
						|
 - The ref may be something like a branch name, and it could contain
 | 
						|
 - ":subdir" if a subtree is wanted. -}
 | 
						|
tree :: Ref -> Repo -> IO (Maybe Sha)
 | 
						|
tree (Ref ref) = extractSha <$$> pipeReadStrict
 | 
						|
	[ Param "rev-parse"
 | 
						|
	, Param "--verify"
 | 
						|
	, Param "--quiet"
 | 
						|
	, Param (decodeBS ref')
 | 
						|
	]
 | 
						|
  where
 | 
						|
	ref' = if ":" `S.isInfixOf` ref
 | 
						|
		then ref
 | 
						|
		-- de-reference commit objects to the tree
 | 
						|
		else ref <> ":"
 | 
						|
 | 
						|
{- Check if the first ref is an ancestor of the second ref. 
 | 
						|
 -
 | 
						|
 - Note that if the two refs point to the same commit, it is considered
 | 
						|
 - to be an ancestor of itself.
 | 
						|
 -}
 | 
						|
isAncestor :: Ref -> Ref -> Repo -> IO Bool
 | 
						|
isAncestor r1 r2 = runBool
 | 
						|
	[ Param "merge-base"
 | 
						|
	, Param "--is-ancestor"
 | 
						|
	, Param (fromRef r1)
 | 
						|
	, Param (fromRef r2)
 | 
						|
	]
 | 
						|
 | 
						|
{- Checks if a String is a legal git ref name.
 | 
						|
 -
 | 
						|
 - The rules for this are complex; see git-check-ref-format(1) -}
 | 
						|
legal :: Bool -> String -> Bool
 | 
						|
legal allowonelevel s = all (== False) illegal
 | 
						|
  where
 | 
						|
	illegal =
 | 
						|
		[ any ("." `isPrefixOf`) pathbits
 | 
						|
		, any (".lock" `isSuffixOf`) pathbits
 | 
						|
		, not allowonelevel && length pathbits < 2
 | 
						|
		, contains ".."
 | 
						|
		, any (\c -> contains [c]) illegalchars
 | 
						|
		, begins "/"
 | 
						|
		, ends "/"
 | 
						|
		, contains "//"
 | 
						|
		, ends "."
 | 
						|
		, contains "@{"
 | 
						|
		, null s
 | 
						|
		]
 | 
						|
	contains v = v `isInfixOf` s
 | 
						|
	ends v = v `isSuffixOf` s
 | 
						|
	begins v = v `isPrefixOf` s
 | 
						|
 | 
						|
	pathbits = splitc '/' s
 | 
						|
	illegalchars = " ~^:?*[\\" ++ controlchars
 | 
						|
	controlchars = chr 0o177 : [chr 0 .. chr (0o40-1)]
 |