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