1-- Copyright (C) 2002-2004 David Roundy 2-- 3-- This program is free software; you can redistribute it and/or modify 4-- it under the terms of the GNU General Public License as published by 5-- the Free Software Foundation; either version 2, or (at your option) 6-- any later version. 7-- 8-- This program is distributed in the hope that it will be useful, 9-- but WITHOUT ANY WARRANTY; without even the implied warranty of 10-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11-- GNU General Public License for more details. 12-- 13-- You should have received a copy of the GNU General Public License 14-- along with this program; see the file COPYING. If not, write to 15-- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 16-- Boston, MA 02110-1301, USA. 17 18{-# LANGUAGE OverloadedStrings #-} 19-- | Helper functions to access option contents. Some of them are here only to 20-- ease the transition from the legacy system where we manually parsed the flag 21-- list to the new(er) option system. At some point this module should be 22-- renamed and the re-exports from "Darcs.UI.Options.All" removed. 23module Darcs.UI.Flags 24 ( F.DarcsFlag 25 , remoteDarcs 26 , diffingOpts 27 , diffOpts 28 , scanKnown 29 , wantGuiPause 30 , isInteractive 31 , willRemoveLogFile 32 , includeBoring 33 , lookForAdds 34 , lookForMoves 35 , lookForReplaces 36 , setDefault 37 , allowConflicts 38 , hasXmlOutput 39 , hasLogfile 40 , quiet 41 , verbose 42 , enumeratePatches 43 44 , fixRemoteRepos 45 , fixUrl 46 , pathsFromArgs 47 , pathSetFromArgs 48 , getRepourl 49 , getAuthor 50 , promptAuthor 51 , getEasyAuthor 52 , getSendmailCmd 53 , fileHelpAuthor 54 , environmentHelpEmail 55 , getSubject 56 , getInReplyTo 57 , getCc 58 , environmentHelpSendmail 59 , getOutput 60 , getDate 61 , workRepo 62 , withNewRepo 63 64 -- * Re-exports 65 , O.compress 66 , O.diffAlgorithm 67 , O.reorder 68 , O.minimize 69 , O.editDescription 70 , O.externalMerge 71 , O.maxCount 72 , O.matchAny 73 , O.withContext 74 , O.allowCaseDifferingFilenames 75 , O.allowWindowsReservedFilenames 76 , O.changesReverse 77 , O.usePacks 78 , O.onlyToFiles 79 , O.amendUnrecord 80 , O.verbosity 81 , O.useCache 82 , O.useIndex 83 , O.umask 84 , O.dryRun 85 , O.runTest 86 , O.testChanges 87 , O.setScriptsExecutable 88 , O.withWorkingDir 89 , O.leaveTestDir 90 , O.remoteRepos 91 , O.cloneKind 92 , O.patchIndexNo 93 , O.patchIndexYes 94 , O.xmlOutput 95 , O.selectDeps 96 , O.author 97 , O.patchFormat 98 , O.charset 99 , O.siblings 100 , O.applyAs 101 , O.enumPatches 102 ) where 103 104import Darcs.Prelude 105 106import Data.List ( intercalate ) 107import Data.List.Ordered ( nubSort ) 108import Data.Maybe 109 ( isJust 110 , maybeToList 111 , isNothing 112 , catMaybes 113 ) 114import Control.Monad ( unless ) 115import System.Directory ( doesDirectoryExist, createDirectory ) 116import System.FilePath.Posix ( (</>) ) 117import System.Environment ( lookupEnv ) 118 119-- Use of RemoteRepo data constructor is harmless here, if not ideal. 120-- See haddocks for fixRemoteRepos below for details. 121import qualified Darcs.UI.Options.Flags as F ( DarcsFlag(RemoteRepo) ) 122import Darcs.UI.Options ( Config, (?), (^), oparse, parseFlags, unparseOpt ) 123import qualified Darcs.UI.Options.All as O 124 125import Darcs.Util.Exception ( catchall ) 126import Darcs.Util.File ( withCurrentDirectory ) 127import Darcs.Util.Prompt 128 ( askUser 129 , askUserListItem 130 ) 131import Darcs.Util.Lock ( writeTextFile ) 132import Darcs.Repository.Flags ( WorkRepo(..) ) 133import Darcs.Repository.Prefs 134 ( getPreflist 135 , getGlobal 136 , globalPrefsDirDoc 137 , globalPrefsDir 138 , prefsDirPath 139 ) 140import Darcs.Util.IsoDate ( getIsoDateTime, cleanLocalDate ) 141import Darcs.Util.Path 142 ( AbsolutePath 143 , AbsolutePathOrStd 144 , toFilePath 145 , makeSubPathOf 146 , ioAbsolute 147 , makeAbsoluteOrStd 148 , AnchoredPath 149 , floatSubPath 150 , inDarcsdir 151 ) 152import Darcs.Util.Printer ( pathlist, putDocLn, text, ($$), (<+>) ) 153import Darcs.Util.Printer.Color ( ePutDocLn ) 154import Darcs.Util.URL ( isValidLocalPath ) 155 156verbose :: Config -> Bool 157verbose = (== O.Verbose) . parseFlags O.verbosity 158 159quiet :: Config -> Bool 160quiet = (== O.Quiet) . parseFlags O.verbosity 161 162remoteDarcs :: Config -> O.RemoteDarcs 163remoteDarcs = O.remoteDarcs . parseFlags O.network 164 165enumeratePatches :: Config -> Bool 166enumeratePatches = (== O.YesEnumPatches) . parseFlags O.enumPatches 167 168diffOpts :: O.UseIndex -> O.LookForAdds -> O.IncludeBoring -> O.DiffAlgorithm -> (O.UseIndex, O.ScanKnown, O.DiffAlgorithm) 169diffOpts use_index look_for_adds include_boring diff_alg = 170 (use_index, scanKnown look_for_adds include_boring, diff_alg) 171 172-- | Non-trivial interaction between options. 173scanKnown :: O.LookForAdds -> O.IncludeBoring -> O.ScanKnown 174scanKnown O.NoLookForAdds _ = O.ScanKnown 175scanKnown O.YesLookForAdds O.NoIncludeBoring = O.ScanAll 176scanKnown O.YesLookForAdds O.YesIncludeBoring = O.ScanBoring 177 178diffingOpts :: Config -> (O.UseIndex, O.ScanKnown, O.DiffAlgorithm) 179diffingOpts flags = diffOpts (O.useIndex ? flags) (lookForAdds flags) 180 (parseFlags O.includeBoring flags) (O.diffAlgorithm ? flags) 181 182-- | This will become dis-entangled as soon as we inline these functions. 183wantGuiPause :: Config -> O.WantGuiPause 184wantGuiPause fs = 185 if (hasDiffCmd fs || hasExternalMerge fs) && hasPause fs 186 then O.YesWantGuiPause 187 else O.NoWantGuiPause 188 where 189 hasDiffCmd = isJust . O.diffCmd . parseFlags O.extDiff 190 hasExternalMerge = (/= O.NoExternalMerge) . parseFlags O.externalMerge 191 hasPause = (== O.YesWantGuiPause) . parseFlags O.pauseForGui 192 193-- | Non-trivial interaction between options. Explicit @-i@ or @-a@ dominates, 194-- else @--count@, @--xml@, or @--dry-run@ imply @-a@, else use the def argument. 195isInteractive :: Bool -> Config -> Bool 196isInteractive def = oparse (O.dryRunXml ^ O.changesFormat ^ O.interactive) decide 197 where 198 decide :: O.DryRun -> O.XmlOutput -> Maybe O.ChangesFormat -> Maybe Bool -> Bool 199 decide _ _ _ (Just True) = True 200 decide _ _ _ (Just False) = False 201 decide _ _ (Just O.CountPatches) Nothing = False 202 decide _ O.YesXml _ Nothing = False 203 decide O.YesDryRun _ _ Nothing = False 204 decide _ _ _ Nothing = def 205 206willRemoveLogFile :: Config -> Bool 207willRemoveLogFile = O._rmlogfile . parseFlags O.logfile 208 209includeBoring :: Config -> Bool 210includeBoring cfg = case parseFlags O.includeBoring cfg of 211 O.NoIncludeBoring -> False 212 O.YesIncludeBoring -> True 213 214lookForAdds :: Config -> O.LookForAdds 215lookForAdds = O.adds . parseFlags O.lookfor 216 217lookForReplaces :: Config -> O.LookForReplaces 218lookForReplaces = O.replaces . parseFlags O.lookfor 219 220lookForMoves :: Config -> O.LookForMoves 221lookForMoves = O.moves . parseFlags O.lookfor 222 223setDefault :: Bool -> Config -> O.SetDefault 224setDefault defYes = maybe def noDef . parseFlags O.setDefault where 225 def = if defYes then O.YesSetDefault False else O.NoSetDefault False 226 noDef yes = if yes then O.YesSetDefault True else O.NoSetDefault True 227 228allowConflicts :: Config -> O.AllowConflicts 229allowConflicts = maybe O.NoAllowConflicts id . parseFlags O.conflictsNo 230 231-- | Ugly. The alternative is to put the remoteRepos accessor into the IO monad, 232-- which is hardly better. 233-- However, accessing the flag list directly here is benign, as we only map 234-- over the list and don't change the order. 235fixRemoteRepos :: AbsolutePath -> Config -> IO Config 236fixRemoteRepos d = mapM fixRemoteRepo where 237 fixRemoteRepo (F.RemoteRepo p) = F.RemoteRepo `fmap` fixUrl d p 238 fixRemoteRepo f = return f 239 240-- | 'fixUrl' takes a String that may be a file path or a URL. 241-- It returns either the URL, or an absolute version of the path. 242fixUrl :: AbsolutePath -> String -> IO String 243fixUrl d f = if isValidLocalPath f 244 then toFilePath `fmap` withCurrentDirectory d (ioAbsolute f) 245 else return f 246 247-- TODO move the following four functions somewhere else, 248-- they have nothing to do with flags 249 250-- | Used by commands that expect arguments to be paths in the current repo. 251-- Invalid paths are dropped and a warning is issued. This may leave no valid 252-- paths to return. Although these commands all fail if there are no remaining 253-- valid paths, they do so in various different ways, issuing error messages 254-- tailored to the command. 255pathsFromArgs :: (AbsolutePath, AbsolutePath) -> [String] -> IO [AnchoredPath] 256pathsFromArgs fps args = catMaybes <$> maybeFixSubPaths fps args 257 258-- | Used by commands that interpret a set of optional path arguments as 259-- "restrict to these paths", which affects patch selection (e.g. in log 260-- command) or selection of subtrees (e.g. in record). Because of the special 261-- meaning of "no arguments", we must distinguish it from "no valid arguments". 262-- A result of 'Nothing' here means "no restriction to the set of paths". If 263-- 'Just' is returned, the set is guaranteed to be non-empty. 264pathSetFromArgs :: (AbsolutePath, AbsolutePath) 265 -> [String] 266 -> IO (Maybe [AnchoredPath]) 267pathSetFromArgs _ [] = return Nothing 268pathSetFromArgs fps args = do 269 pathSet <- nubSort . catMaybes <$> maybeFixSubPaths fps args 270 case pathSet of 271 [] -> fail "No valid arguments were given." 272 _ -> return $ Just pathSet 273 274-- | @maybeFixSubPaths (repo_path, orig_path) file_paths@ tries to turn 275-- @file_paths@ into 'SubPath's, taking into account the repository path and 276-- the original path from which darcs was invoked. 277-- 278-- A 'SubPath' is a path /under/ (or inside) the repo path. This does /not/ 279-- mean it must exist as a file or directory, nor that the path has been added 280-- to the repository; it merely means that it /could/ be added. 281-- 282-- When converting a relative path to an absolute one, this function first tries 283-- to interpret the relative path with respect to the current working directory. 284-- If that fails, it tries to interpret it with respect to the repository 285-- directory. Only when that fails does it put a @Nothing@ in the result at the 286-- position of the path that cannot be converted. 287-- 288-- It is intended for validating file arguments to darcs commands. 289maybeFixSubPaths :: (AbsolutePath, AbsolutePath) 290 -> [String] 291 -> IO [Maybe AnchoredPath] 292maybeFixSubPaths (r, o) fs = do 293 fixedFs <- mapM (fmap dropInDarcsdir . fixit) fs 294 let bads = snd . unzip . filter (isNothing . fst) $ zip fixedFs fs 295 unless (null bads) $ 296 ePutDocLn $ text "Ignoring invalid repository paths:" <+> pathlist bads 297 return fixedFs 298 where 299 dropInDarcsdir (Just p) | inDarcsdir p = Nothing 300 dropInDarcsdir mp = mp 301 -- special case here because fixit otherwise converts 302 -- "" to (SubPath "."), which is a valid path 303 fixit "" = return Nothing 304 fixit p = do ap <- withCurrentDirectory o $ ioAbsolute p 305 case makeSubPathOf r ap of 306 Just sp -> return $ Just $ floatSubPath sp 307 Nothing -> do 308 absolutePathByRepodir <- withCurrentDirectory r $ ioAbsolute p 309 return $ floatSubPath <$> makeSubPathOf r absolutePathByRepodir 310 311-- | 'getRepourl' takes a list of flags and returns the url of the 312-- repository specified by @Repodir \"directory\"@ in that list of flags, if any. 313-- This flag is present if darcs was invoked with @--repodir=DIRECTORY@ 314getRepourl :: Config -> Maybe String 315getRepourl fs = case parseFlags O.possiblyRemoteRepo fs of 316 Nothing -> Nothing 317 Just d -> if not (isValidLocalPath d) then Just d else Nothing 318 319fileHelpAuthor :: [String] 320fileHelpAuthor = [ 321 "Each patch is attributed to its author, usually by email address (for", 322 "example, `Fred Bloggs <fred@example.net>`). Darcs looks in several", 323 "places for this author string: the `--author` option, the files", 324 "`_darcs/prefs/author` (in the repository) and `" ++ globalPrefsDirDoc ++ "author` (in your", 325 "home directory), and the environment variables `$DARCS_EMAIL` and", 326 "`$EMAIL`. If none of those exist, Darcs will prompt you for an author", 327 "string and write it to `" ++ globalPrefsDirDoc ++ "author`. Note that if you have more", 328 "than one email address, you can put them all in `" ++ globalPrefsDirDoc ++ "author`,", 329 "one author per line. Darcs will still prompt you for an author, but it", 330 "allows you to select from the list, or to type in an alternative." 331 ] 332 333environmentHelpEmail :: ([String], [String]) 334environmentHelpEmail = (["DARCS_EMAIL","EMAIL"], fileHelpAuthor) 335 336-- | 'getAuthor' takes a list of flags and returns the author of the 337-- change specified by @Author \"Leo Tolstoy\"@ in that list of flags, if any. 338-- Otherwise, if @Pipe@ is present, asks the user who is the author and 339-- returns the answer. If neither are present, try to guess the author, 340-- from repository or global preference files or environment variables, 341-- and if it's not possible, ask the user. 342getAuthor :: Maybe String -> Bool -> IO String 343getAuthor (Just author) _ = return author 344getAuthor Nothing pipe = 345 if pipe 346 then askUser "Who is the author? " 347 else promptAuthor True False 348 349-- | 'promptAuthor' try to guess the author, from repository or 350-- global preference files or environment variables, and 351-- if it's not possible or alwaysAsk parameter is true, ask the user. 352-- If store parameter is true, the new author is added into 353-- @_darcs/prefs@. 354promptAuthor :: Bool -- Store the new author 355 -> Bool -- Author selection even if already stored 356 -> IO String 357promptAuthor store alwaysAsk = do 358 as <- getEasyAuthor 359 case as of 360 [a] -> if alwaysAsk then 361 askForAuthor False (fancyPrompt as) (fancyPrompt as) 362 else return a 363 [] -> askForAuthor True shortPrompt longPrompt 364 _ -> askForAuthor False (fancyPrompt as) (fancyPrompt as) 365 where 366 shortPrompt = askUser "What is your email address? " 367 longPrompt = askUser "What is your email address (e.g. Fred Bloggs <fred@example.net>)? " 368 fancyPrompt xs = 369 do putDocLn $ text "" $$ 370 text "You have saved the following email addresses to your global settings:" 371 str <- askUserListItem "Please select an email address for this repository: " (xs ++ ["Other"]) 372 if str == "Other" 373 then longPrompt 374 else return str 375 askForAuthor storeGlobal askfn1 askfn2 = do 376 aminrepo <- doesDirectoryExist prefsDirPath 377 if aminrepo && store then do 378 prefsdir <- if storeGlobal 379 then tryGlobalPrefsDir 380 else return prefsDirPath 381 putDocLn $ 382 text "Each patch is attributed to its author, usually by email address (for" $$ 383 text "example, `Fred Bloggs <fred@example.net>'). Darcs could not determine" $$ 384 text "your email address, so you will be prompted for it." $$ 385 text "" $$ 386 text ("Your address will be stored in " ++ prefsdir) 387 if prefsdir /= prefsDirPath then 388 putDocLn $ 389 text "It will be used for all patches you record in ALL repositories." $$ 390 text ("If you move that file to " ++ prefsDirPath </> "author" ++ ", it will") $$ 391 text "be used for patches recorded in this repository only." 392 else 393 putDocLn $ 394 text "It will be used for all patches you record in this repository only." $$ 395 text ("If you move that file to " ++ globalPrefsDirDoc ++ "author, it will") $$ 396 text "be used for all patches recorded in ALL repositories." 397 add <- askfn1 398 writeTextFile (prefsdir </> "author") $ 399 unlines ["# " ++ line | line <- fileHelpAuthor] ++ "\n" ++ add 400 return add 401 else askfn2 402 tryGlobalPrefsDir = do 403 maybeprefsdir <- globalPrefsDir 404 case maybeprefsdir of 405 Nothing -> do 406 putStrLn "WARNING: Global preference directory could not be found." 407 return prefsDirPath 408 Just dir -> do exists <- doesDirectoryExist dir 409 unless exists $ createDirectory dir 410 return dir 411 412-- | 'getEasyAuthor' tries to get the author name first from the repository 413-- preferences, then from global preferences, then from environment variables. 414-- Returns @[]@ if it could not get it. Note that it may only return multiple 415-- possibilities when reading from global preferences. 416getEasyAuthor :: IO [String] 417getEasyAuthor = 418 firstNotNullIO [ (take 1 . nonblank) `fmap` getPreflist "author" 419 , nonblank `fmap` getGlobal "author" 420 , maybeToList `fmap` lookupEnv "DARCS_EMAIL" 421 , maybeToList `fmap` lookupEnv "EMAIL" 422 ] 423 where 424 nonblank = filter (not . null) 425 -- this could perhaps be simplified with Control.Monad 426 -- but note that we do NOT want to concatenate the results 427 firstNotNullIO [] = return [] 428 firstNotNullIO (e:es) = do 429 v <- e `catchall` return [] 430 if null v then firstNotNullIO es else return v 431 432getDate :: Bool -> IO String 433getDate hasPipe = if hasPipe then cleanLocalDate =<< askUser "What is the date? " 434 else getIsoDateTime 435 436environmentHelpSendmail :: ([String], [String]) 437environmentHelpSendmail = (["SENDMAIL"], [ 438 "On Unix, the `darcs send` command relies on sendmail(8). The", 439 "`--sendmail-command` or $SENDMAIL environment variable can be used to", 440 "provide an explicit path to this program; otherwise the standard", 441 "locations /usr/sbin/sendmail and /usr/lib/sendmail will be tried."]) 442-- FIXME: mention the following also: 443-- * sendmail(8) is not sendmail-specific; 444-- * nowadays, desktops often have no MTA or an unconfigured MTA -- 445-- which is awful, because it accepts mail but doesn't relay it; 446-- * in this case, can be a sendmail(8)-emulating wrapper on top of an 447-- MUA that sends mail directly to a smarthost; and 448-- * on a multi-user system without an MTA and on which you haven't 449-- got root, can be msmtp. 450 451-- |'getSendmailCmd' takes a list of flags and returns the sendmail command 452-- to be used by @darcs send@. Looks for a command specified by 453-- @SendmailCmd \"command\"@ in that list of flags, if any. 454-- This flag is present if darcs was invoked with @--sendmail-command=COMMAND@ 455-- Alternatively the user can set @$S@@ENDMAIL@ which will be used as a 456-- fallback if present. 457getSendmailCmd :: Config -> IO String 458getSendmailCmd fs = case parseFlags O.sendmailCmd fs of 459 Just cmd -> return cmd 460 Nothing -> fmap (maybe "" id) $ lookupEnv "SENDMAIL" 461 462-- | Accessor for output option 463getOutput :: Config -> FilePath -> Maybe AbsolutePathOrStd 464getOutput fs fp = fmap go (parseFlags O.output fs) where 465 go (O.Output ap) = ap 466 go (O.OutputAutoName ap) = makeAbsoluteOrStd ap fp 467 468-- |'getSubject' takes a list of flags and returns the subject of the mail 469-- to be sent by @darcs send@. Looks for a subject specified by 470-- @Subject \"subject\"@ in that list of flags, if any. 471-- This flag is present if darcs was invoked with @--subject=SUBJECT@ 472getSubject :: Config -> Maybe String 473getSubject = O._subject . parseFlags O.headerFields 474 475-- |'getCc' takes a list of flags and returns the addresses to send a copy of 476-- the patch bundle to when using @darcs send@. 477-- looks for a cc address specified by @Cc \"address\"@ in that list of flags. 478-- Returns the addresses as a comma separated string. 479getCc :: Config -> String 480getCc = intercalate " , " . O._cc . parseFlags O.headerFields 481 482getInReplyTo :: Config -> Maybe String 483getInReplyTo = O._inReplyTo . parseFlags O.headerFields 484 485hasXmlOutput :: Config -> Bool 486hasXmlOutput = (== O.YesXml) . parseFlags O.xmlOutput 487 488hasLogfile :: Config -> Maybe AbsolutePath 489hasLogfile = O._logfile . parseFlags O.logfile 490 491workRepo :: Config -> WorkRepo 492workRepo = oparse (O.repoDir ^ O.possiblyRemoteRepo) go 493 where 494 go (Just s) _ = WorkRepoDir s 495 go Nothing (Just s) = WorkRepoPossibleURL s 496 go Nothing Nothing = WorkRepoCurrentDir 497 498withNewRepo :: String -> Config -> Config 499withNewRepo dir = unparseOpt O.newRepo (Just dir) 500