-- Copyright (C) 2003-2005 David Roundy -- -- This program is free software; you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation; either version 2, or (at your option) -- any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program; see the file COPYING. If not, write to -- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -- Boston, MA 02110-1301, USA. module Darcs.UI.Commands.Apply ( apply, applyCmd , getPatchBundle -- used by darcsden ) where import Prelude () import Darcs.Prelude import System.Exit ( exitSuccess ) import Control.Monad ( when ) import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd, hopefullyM, info ) import Darcs.UI.Commands ( DarcsCommand(..), withStdOpts , putVerbose , amInHashedRepository ) import Darcs.UI.Completion ( fileArgs ) import Darcs.UI.Flags ( DarcsFlag , happyForwarding, changesReverse, verbosity, useCache, dryRun , reorder, umask , fixUrl, getCc, getSendmailCmd , withContext, reply ) import Darcs.UI.Options ( (^), odesc, ocheck, onormalise, defaultFlags, parseFlags, (?) ) import qualified Darcs.UI.Options.All as O import Darcs.Repository.Flags ( UpdateWorking(..) ) import Darcs.Util.Path ( toFilePath, AbsolutePath ) import Darcs.Repository ( Repository , SealedPatchSet , withRepoLock , readRepo , filterOutConflicts ) import Darcs.Patch.Set ( Origin, patchSet2RL ) import Darcs.Patch ( IsRepoType, RepoPatch ) import Darcs.Patch.Apply( ApplyState ) import Darcs.Patch.Info ( PatchInfo, displayPatchInfo ) import Darcs.Patch.Witnesses.Ordered ( RL(..), (:\/:)(..), (:>)(..) , mapRL, nullFL, reverseFL ) import Darcs.Patch.Witnesses.Sealed ( Sealed(Sealed) ) import Darcs.Util.ByteString ( linesPS, unlinesPS, gzReadStdin ) import Data.List( (\\) ) import qualified Data.ByteString as B (ByteString, null, init, take, drop) import qualified Data.ByteString.Char8 as BC (unpack, last, pack) import Darcs.Util.Download ( Cachable(Uncachable) ) import Darcs.Util.External ( gzFetchFilePS ) import Darcs.UI.External ( sendEmailDoc , resendEmail , verifyPS ) import Darcs.UI.Email ( readEmail ) import Darcs.Patch.Depends ( findUncommon, findCommonWithThem ) import Darcs.UI.ApplyPatches ( PatchApplier(..), StandardPatchApplier(..), PatchProxy ) import Darcs.UI.SelectChanges ( WhichChanges(..) , runSelection , selectionContext ) import qualified Darcs.UI.SelectChanges as S import Darcs.Patch.Bundle ( scanBundle ) import Darcs.Util.Printer ( packedString, vcat, text, empty , renderString ) import Darcs.Util.Tree( Tree ) applyDescription :: String applyDescription = "Apply a patch bundle created by `darcs send'." applyHelp :: String applyHelp = unlines [ "The `darcs apply` command takes a patch bundle and attempts to insert" , "it into the current repository. In addition to invoking it directly" , "on bundles created by `darcs send`, it is used internally by `darcs" , "push` on the remote end of an SSH connection." , "" , "If no file is supplied, the bundle is read from standard input." , "" , "If given an email instead of a patch bundle, Darcs will look for the" , "bundle as a MIME attachment to that email. Currently this will fail" , "if the MIME boundary is rewritten, such as in Courier and Mail.app." , "" , "If the `--reply noreply@example.net` option is used, and the bundle is" , "attached to an email, Darcs will send a report (indicating success or" , "failure) to the sender of the bundle (the `To` field). The argument to" , "noreply is the address the report will appear to originate FROM." , "" , "The `--cc` option will cause the report to be CC'd to another address," , "for example `--cc reports@lists.example.net,admin@lists.example.net`." , "Using `--cc` without `--reply` is undefined." , "" , "If you want to use a command different from the default one for sending mail," , "you need to specify a command line with the `--sendmail-command` option." , "The command line can contain the format specifier `%t` for to" , "and you can add `%<` to the end of the command line if the command" , "expects the complete mail on standard input. For example, the command line" , "for msmtp looks like this:" , "" , " msmtp -t %<" , "" , "If gpg(1) is installed, you can use `--verify pubring.gpg` to reject" , "bundles that aren't signed by a key in `pubring.gpg`." , "" , "If `--test` is supplied and a test is defined (see `darcs setpref`), the" , "bundle will be rejected if the test fails after applying it. In that" , "case, the rejection email from `--reply` will include the test output." ] stdindefault :: a -> [String] -> IO [String] stdindefault _ [] = return ["-"] stdindefault _ x = return x apply :: DarcsCommand [DarcsFlag] apply = DarcsCommand { commandProgramName = "darcs" , commandName = "apply" , commandHelp = applyHelp ++ "\n" ++ applyHelp' , commandDescription = applyDescription , commandExtraArgs = 1 , commandExtraArgHelp = [""] , commandCommand = applyCmd StandardPatchApplier , commandPrereq = amInHashedRepository , commandCompleteArgs = fileArgs , commandArgdefaults = const stdindefault , commandAdvancedOptions = odesc applyAdvancedOpts , commandBasicOptions = odesc applyBasicOpts , commandDefaults = defaultFlags applyOpts , commandCheckOptions = ocheck applyOpts , commandParseOptions = onormalise applyOpts } where applyBasicOpts = O.verify ^ O.reorder ^ O.interactive ^ O.dryRunXml ^ O.matchSeveral ^ O.conflictsNo ^ O.externalMerge ^ O.runTest ^ O.leaveTestDir ^ O.repoDir ^ O.diffAlgorithm applyAdvancedOpts = O.reply ^ O.ccApply ^ O.happyForwarding ^ O.sendmail ^ O.useIndex ^ O.compress ^ O.setScriptsExecutable ^ O.umask ^ O.restrictPaths ^ O.changesReverse ^ O.pauseForGui applyOpts = applyBasicOpts `withStdOpts` applyAdvancedOpts applyCmd :: PatchApplier pa => pa -> (AbsolutePath, AbsolutePath) -> [DarcsFlag] -> [String] -> IO () applyCmd _ _ _ [""] = fail "Empty filename argument given to apply!" applyCmd patchApplier _ opts ["-"] = withRepoLock (dryRun ? opts) (useCache ? opts) YesUpdateWorking (umask ? opts) $ repoJob patchApplier opts $ \patchProxy repository -> do -- for darcs users who try out 'darcs apply' without any arguments putVerbose opts $ text "reading patch bundle from stdin..." bundle <- gzReadStdin applyCmdCommon patchApplier patchProxy opts bundle repository applyCmd patchApplier (_,o) opts [unfixed_patchesfile] = withRepoLock (dryRun ? opts) (useCache ? opts) YesUpdateWorking (umask ? opts) $ repoJob patchApplier opts $ \patchProxy repository -> do patchesfile <- fixUrl o unfixed_patchesfile bundle <- gzFetchFilePS (toFilePath patchesfile) Uncachable applyCmdCommon patchApplier patchProxy opts bundle repository applyCmd _ _ _ _ = impossible applyCmdCommon :: forall rt pa p wR wU . ( PatchApplier pa, RepoPatch p, ApplyState p ~ Tree , ApplierRepoTypeConstraint pa rt, IsRepoType rt ) => pa -> PatchProxy p -> [DarcsFlag] -> B.ByteString -> Repository rt p wR wU wR -> IO () applyCmdCommon patchApplier patchProxy opts bundle repository = do let from_whom = getFrom bundle us <- readRepo repository either_them <- getPatchBundle opts bundle Sealed them <- case either_them of Right t -> return t Left er -> do forwarded <- considerForwarding opts bundle if forwarded then exitSuccess else fail er common :> _ <- return $ findCommonWithThem us them -- all patches that are in "them" and not in "common" need to be available; check that let common_i = mapRL info $ patchSet2RL common them_i = mapRL info $ patchSet2RL them required = them_i \\ common_i -- FIXME quadratic? check :: RL (PatchInfoAnd rt p) wX wY -> [PatchInfo] -> IO () check (ps' :<: p) bad = case hopefullyM p of Nothing | info p `elem` required -> check ps' (info p : bad) _ -> check ps' bad check NilRL [] = return () check NilRL bad = fail . renderString $ vcat $ map displayPatchInfo bad ++ [ text "\nFATAL: Cannot apply this bundle. We are missing the above patches." ] check (patchSet2RL them) [] (us':\/:them') <- return $ findUncommon us them (hadConflicts, Sealed their_ps) <- if O.conflictsNo ? opts == Nothing -- skip conflicts then filterOutConflicts (reverseFL us') repository them' else return (False, Sealed them') when hadConflicts $ putStrLn "Skipping some patches which would cause conflicts." when (nullFL their_ps) $ do if hadConflicts then putStrLn ("All new patches of the bundle cause conflicts. " ++ "Nothing to do.") >> exitSuccess else putStrLn ("All these patches have already been applied. " ++ "Nothing to do.") >> when (reorder ? opts /= O.Reorder) exitSuccess let direction = if changesReverse ? opts then FirstReversed else First context = selectionContext direction "apply" (patchSelOpts opts) Nothing Nothing (to_be_applied :> _) <- runSelection their_ps context applyPatches patchApplier patchProxy "apply" opts from_whom repository us' to_be_applied -- see the default (False) for the option -- where fixed_opts = if Interactive `elem` opts -- then opts -- else All : opts getPatchBundle :: RepoPatch p => [DarcsFlag] -> B.ByteString -> IO (Either String (SealedPatchSet rt p Origin)) getPatchBundle opts fps = do let opt_verify = parseFlags O.verify opts mps <- verifyPS opt_verify $ readEmail fps mops <- verifyPS opt_verify fps case (mps, mops) of (Nothing, Nothing) -> return $ Left "Patch bundle not properly signed, or gpg failed." (Just bundle, Nothing) -> return $ scanBundle bundle (Nothing, Just bundle) -> return $ scanBundle bundle -- We use careful_scan_bundle only below because in either of the two -- above case we know the patch was signed, so it really shouldn't -- need stripping of CRs. (Just ps1, Just ps2) -> case careful_scan_bundle ps1 of Left _ -> return $ careful_scan_bundle ps2 Right x -> return $ Right x where careful_scan_bundle bundle = case scanBundle bundle of Left e -> case scanBundle $ stripCrPS bundle of Right x -> Right x _ -> Left e x -> x stripCrPS :: B.ByteString -> B.ByteString stripCrPS bundle = unlinesPS $ map stripline $ linesPS bundle stripline p | B.null p = p | BC.last p == '\r' = B.init p | otherwise = p applyHelp' :: String applyHelp' = "A patch bundle may introduce unresolved conflicts with existing\n" ++ "patches or with the working tree. By default, Darcs will add conflict\n" ++ "markers (see `darcs mark-conflicts`).\n" ++ "\n" ++ "The `--external-merge` option lets you resolve these conflicts\n" ++ "using an external merge tool. In the option, `%a` is replaced with\n" ++ "the common ancestor (merge base), `%1` with the first version, `%2`\n" ++ "with the second version, and `%o` with the path where your resolved\n" ++ "content should go. For example, to use the xxdiff visual merge tool\n" ++ "you'd specify: `--external-merge='xxdiff -m -O -M %o %1 %a %2'`\n" ++ "\n" ++ "The `--allow-conflicts` option will skip conflict marking; this is\n" ++ "useful when you want to treat a repository as just a bunch of patches,\n" ++ "such as using `darcs pull --union` to download of your co-workers\n" ++ "patches before going offline.\n" ++ "\n" ++ "This can mess up unrecorded changes in the working tree, forcing you\n" ++ "to resolve the conflict immediately. To simply reject bundles that\n" ++ "introduce unresolved conflicts, using the `--dont-allow-conflicts`\n" ++ "option. Making this the default in push-based workflows is strongly\n" ++ "recommended.\n" ++ "\n" ++ "Unlike most Darcs commands, `darcs apply` defaults to `--all`. Use the\n" ++ "`--interactive` option to pick which patches to apply from a bundle.\n" getFrom :: B.ByteString -> String getFrom bundle = readFrom $ linesPS bundle where readFrom [] = "" readFrom (x:xs) | B.take 5 x == fromStart = BC.unpack $ B.drop 5 x | otherwise = readFrom xs forwardingMessage :: B.ByteString forwardingMessage = BC.pack $ "The following patch was either unsigned, or signed by a non-allowed\n"++ "key, or there was a GPG failure.\n" considerForwarding :: [DarcsFlag] -> B.ByteString -> IO Bool considerForwarding opts bundle = case reply ? opts of Nothing -> return False Just from -> case break is_from (linesPS bundle) of (m1, f:m2) -> let m_lines = forwardingMessage:m1 ++ m2 m' = unlinesPS m_lines f' = BC.unpack (B.drop 5 f) in if from == f' || from == init f' then return False -- Refuse possible email loop. else do scmd <- getSendmailCmd opts if happyForwarding ? opts then resendEmail from scmd bundle else sendEmailDoc f' from "A forwarded darcs patch" cc scmd (Just (empty,empty)) (packedString m') return True _ -> return False -- Don't forward emails lacking headers! where cc = getCc opts is_from l = B.take 5 l == fromStart fromStart :: B.ByteString fromStart = BC.pack "From:" patchSelOpts :: [DarcsFlag] -> S.PatchSelectionOptions patchSelOpts flags = S.PatchSelectionOptions { S.verbosity = verbosity ? flags , S.matchFlags = parseFlags O.matchSeveral flags , S.interactive = maybeIsInteractive flags , S.selectDeps = O.PromptDeps -- option not supported, use default , S.summary = O.NoSummary -- option not supported, use default , S.withContext = withContext ? flags } maybeIsInteractive :: [DarcsFlag] -> Bool maybeIsInteractive = maybe False id . parseFlags O.interactive