% 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.
\darcsCommand{apply}
\begin{code}
module Darcs.Commands.Apply ( apply ) where
import System.Exit ( ExitCode(..), exitWith )
import Prelude hiding ( catch )
import System.IO ( hClose, stdin, stdout, stderr )
import Control.Exception ( catch, throw, Exception( ExitException ) )
import Control.Monad ( when )
import Darcs.Hopefully ( PatchInfoAnd, n2pia, conscientiously, info )
import Darcs.SignalHandler ( withSignalsBlocked )
import Darcs.Commands ( DarcsCommand(..), putVerbose )
import Darcs.CommandsAux ( check_paths )
import Darcs.Arguments ( DarcsFlag( Reply, Interactive, All ),
definePatches,
getCc, workingRepoDir,
notest, nocompress, applyConflictOptions,
useExternalMerge,
ignoretimes, getSendmailCmd,
reply, verify, listFiles,
fixFilePathOrStd, umaskOption,
allInteractive, sendmailCmd,
leaveTestDir, happyForwarding,
dryRun, printDryRunMessageAndExit,
setScriptsExecutableOption, restrictPaths
)
import Darcs.Flags(doHappyForwarding)
import qualified Darcs.Arguments as DarcsArguments ( ccApply )
import Darcs.RepoPath ( toFilePath, useAbsoluteOrStd )
import Darcs.Repository ( Repository, SealedPatchSet, withRepoLock, ($-), amInRepository,
tentativelyMergePatches,
read_repo,
finalizeRepositoryChanges,
applyToWorking, invalidateIndex
)
import Darcs.Patch ( RepoPatch, description )
import Darcs.Patch.Info ( PatchInfo, human_friendly )
import Darcs.Witnesses.Ordered ( FL, RL, (:\/:)(..), (:>)(..),
mapFL, nullFL, mapFL_FL, mapRL, concatRL, reverseRL )
import ByteStringUtils ( linesPS, unlinesPS )
import qualified Data.ByteString as B (ByteString, null, readFile, hGetContents, init, take, drop)
import qualified Data.ByteString.Char8 as BC (unpack, last, pack)
import Darcs.External ( sendEmail, sendEmailDoc, resendEmail,
verifyPS )
import Darcs.Email ( read_email )
import Darcs.Lock ( withStdoutTemp, readBinFile )
import Darcs.Patch.Depends ( get_common_and_uncommon_or_missing )
import Darcs.SelectChanges ( with_selected_changes, filterOutConflicts )
import Darcs.Patch.Bundle ( scan_bundle )
import Darcs.Witnesses.Sealed ( Sealed(Sealed) )
import Printer ( packedString, vcat, text, ($$), errorDoc, empty )
#include "impossible.h"
#include "gadts.h"
applyDescription :: String
applyDescription = "Apply a patch bundle created by `darcs send'."
applyHelp :: String
applyHelp =
"The `darcs apply' command takes a patch bundle and attempts to insert\n" ++
"it into the current repository. In addition to invoking it directly\n" ++
"on bundles created by `darcs send', it is used internally by `darcs\n" ++
"push' and `darcs put' on the remote end of an SSH connection.\n" ++
"\n" ++
"If no file is supplied, the bundle is read from standard input.\n" ++
"\n" ++
"If given an email instead of a patch bundle, Darcs will look for the\n" ++
"bundle as a MIME attachment to that email. Currently this will fail\n" ++
"if the MIME boundary is rewritten, such as in Courier and Mail.app.\n" ++
"\n" ++
"If the `--reply noreply@example.net' option is used, and the bundle is\n" ++
"attached to an email, Darcs will send a report (indicating success or\n" ++
"failure) to the sender of the bundle (the To field). The argument to\n" ++
"noreply is the address the report will appear to originate FROM.\n" ++
"\n" ++
"The --cc option will cause the report to be CC'd to another address,\n" ++
"for example `--cc reports@lists.example.net,admin@lists.example.net'.\n" ++
"Using --cc without --reply is undefined.\n" ++
"\n" ++
"If gpg(1) is installed, you can use `--verify pubring.gpg' to reject\n" ++
"bundles that aren't signed by a key in pubring.gpg.\n" ++
"\n" ++
"If --test is supplied and a test is defined (see `darcs setpref'), the\n" ++
"bundle will be rejected if the test fails after applying it. In that\n" ++
"case, the rejection email from --reply will include the test output.\n"
stdindefault :: a -> [String] -> IO [String]
stdindefault _ [] = return ["-"]
stdindefault _ x = return x
apply :: DarcsCommand
apply = DarcsCommand {commandName = "apply",
commandHelp = applyHelp ++ "\n" ++ applyHelp',
commandDescription = applyDescription,
commandExtraArgs = 1,
commandExtraArgHelp = ["<PATCHFILE>"],
commandCommand = applyCmd,
commandPrereq = amInRepository,
commandGetArgPossibilities = listFiles,
commandArgdefaults = const stdindefault,
commandAdvancedOptions = [reply, DarcsArguments.ccApply,
happyForwarding,
sendmailCmd,
ignoretimes, nocompress,
setScriptsExecutableOption, umaskOption,
restrictPaths],
commandBasicOptions = [verify,
allInteractive]++dryRun++
[applyConflictOptions,
useExternalMerge,
notest,
leaveTestDir,
workingRepoDir]}
applyCmd :: [DarcsFlag] -> [String] -> IO ()
applyCmd _ [""] = fail "Empty filename argument given to apply!"
applyCmd opts [unfixed_patchesfile] = withRepoLock opts $- \repository -> do
patchesfile <- fixFilePathOrStd opts unfixed_patchesfile
ps <- useAbsoluteOrStd (B.readFile . toFilePath) (B.hGetContents stdin) patchesfile
let from_whom = getFrom ps
us <- read_repo repository
either_them <- getPatchBundle opts ps
Sealed them
<- case either_them of
Right t -> return t
Left er -> do forwarded <- considerForwarding opts ps
if forwarded
then exitWith ExitSuccess
else fail er
(_, us':\/:them') <- case get_common_and_uncommon_or_missing (us, them) of
Left pinfo ->
if pinfo `elem` mapRL info (concatRL us)
then cannotApplyPartialRepo pinfo ""
else cannotApplyMissing pinfo
Right x -> return x
let their_ps = mapFL_FL (n2pia . conscientiously (text ("We cannot apply this patch "
++"bundle, since we're missing:") $$))
$ reverseRL them'
(hadConflicts, Sealed their_ps_filtered) <- filterOutConflicts opts us' repository their_ps
when hadConflicts $ putStrLn "Skipping some patches which would cause conflicts."
when (nullFL their_ps_filtered) $
do putStr $ "All these patches have already been applied. " ++
"Nothing to do.\n"
exitWith ExitSuccess
with_selected_changes "apply" fixed_opts Nothing their_ps_filtered $
\ (to_be_applied:>_) ->
applyItNow opts from_whom repository us' to_be_applied
where fixed_opts = if Interactive `elem` opts
then opts
else All : opts
applyCmd _ _ = impossible
applyItNow :: FORALL(p r u t x y z) RepoPatch p =>
[DarcsFlag] -> String -> Repository p C(r u t)
-> RL (PatchInfoAnd p) C(x r) -> FL (PatchInfoAnd p) C(x z) -> IO ()
applyItNow opts from_whom repository us' to_be_applied = do
printDryRunMessageAndExit "apply" opts to_be_applied
when (nullFL to_be_applied) $
do putStrLn "You don't want to apply any patches, so I'm exiting!"
exitWith ExitSuccess
check_paths opts to_be_applied
redirectOutput opts from_whom $ do
putVerbose opts $ text "We have the following extra patches:"
putVerbose opts . vcat $ mapRL description us'
putVerbose opts $ text "Will apply the following patches:"
putVerbose opts . vcat $ mapFL description to_be_applied
definePatches to_be_applied
Sealed pw <- tentativelyMergePatches repository "apply" opts
(reverseRL us') to_be_applied
invalidateIndex repository
withSignalsBlocked $ do finalizeRepositoryChanges repository
applyToWorking repository opts pw `catch` \e ->
fail ("Error applying patch to working dir:\n" ++ show e)
putStrLn "Finished applying..."
cannotApplyMissing :: PatchInfo -> a
cannotApplyMissing pinfo
= errorDoc $ text "Cannot apply this patch bundle, since we're missing:"
$$ human_friendly pinfo
cannotApplyPartialRepo :: PatchInfo -> String -> a
cannotApplyPartialRepo pinfo e
= errorDoc $ text ("Cannot apply this patch bundle, "
++ "this is a \"--partial repository")
$$ text "We don't have the following patch:"
$$ human_friendly pinfo $$ text e
\end{code}
Darcs apply accepts a single argument, which is the name of the patch
file to be applied. If you omit this argument, the patch is read from
standard input. Darcs also interprets an argument of `\-' to mean it
should read the file from standard input. This allows you to use apply
with a pipe from your email program, for example.
\begin{options}
--verify
\end{options}
\begin{code}
getPatchBundle :: RepoPatch p => [DarcsFlag] -> B.ByteString
-> IO (Either String (SealedPatchSet p))
getPatchBundle opts fps = do
mps <- verifyPS opts $ read_email fps
mops <- verifyPS opts fps
case (mps, mops) of
(Nothing, Nothing) ->
return $ Left "Patch bundle not properly signed, or gpg failed."
(Just ps, Nothing) -> return $ scan_bundle ps
(Nothing, Just ps) -> return $ scan_bundle ps
(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 ps =
case scan_bundle ps of
Left e -> case scan_bundle $ stripCrPS ps of
Right x -> Right x
_ -> Left e
x -> x
stripCrPS :: B.ByteString -> B.ByteString
stripCrPS ps = unlinesPS $ map stripline $ linesPS ps
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 --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"
\end{code}
\begin{options}
--external-merge
\end{options}
You can use an external interactive merge tool to resolve conflicts with the
flag \verb!--external-merge!. For more details see
subsection~\ref{resolution}.
\begin{options}
--sendmail-command
\end{options}
If you want to use a command different from the default one for sending mail,
you need to specify a command line with the \verb!--sendmail-command! option.
The command line can contain the format specifier \verb!%t! for to
and you can add \verb!%getFrom :: B.ByteString -> String
getFrom ps = readFrom $ linesPS ps
where readFrom [] = ""
readFrom (x:xs)
| B.take 5 x == fromStart = BC.unpack $ B.drop 5 x
| otherwise = readFrom xs
redirectOutput :: [DarcsFlag] -> String -> IO a -> IO a
redirectOutput opts to doit = ro opts
where
cc = getCc opts
ro [] = doit
ro (Reply f:_) =
withStdoutTemp $ \tempf-> do {a <- doit;
hClose stdout;
hClose stderr;
return a;
} `catch` (sendit tempf)
where sendit tempf e@(ExitException ExitSuccess) =
do sendSanitizedEmail opts f to "Patch applied" cc tempf
throwIO e
sendit tempf (ExitException _) =
do sendSanitizedEmail opts f to "Patch failed!" cc tempf
throwIO $ ExitException ExitSuccess
sendit tempf e =
do sendSanitizedEmail opts f to "Darcs error applying patch!" cc $
tempf ++ "\n\nCaught exception:\n"++
show e++"\n"
throwIO $ ExitException ExitSuccess
ro (_:fs) = ro fs
sendSanitizedEmail :: [DarcsFlag] -> String -> String -> String -> String -> String -> IO ()
sendSanitizedEmail opts file to subject cc mailtext =
do scmd <- getSendmailCmd opts
body <- sanitizeFile mailtext
sendEmail file to subject cc scmd body
sanitizeFile :: FilePath -> IO String
sanitizeFile f = sanitize `fmap` readBinFile f
where sanitize s = wash $ remove_backspaces "" s
wash ('\000':s) = "\\NUL" ++ wash s
wash ('\026':s) = "\\EOF" ++ wash s
wash (c:cs) = c : wash cs
wash [] = []
remove_backspaces rev_sofar "" = reverse rev_sofar
remove_backspaces (_:rs) ('\008':s) = remove_backspaces rs s
remove_backspaces "" ('\008':s) = remove_backspaces "" s
remove_backspaces rs (s:ss) = remove_backspaces (s:rs) ss
throwIO :: Exception -> IO a
throwIO e = return $ throw e
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 m = cf opts (getCc opts)
where cf [] _ = return False
cf (Reply t:_) cc =
case break is_from (linesPS m) of
(m1, f:m2) ->
let m_lines = forwardingMessage:m1 ++ m2
m' = unlinesPS m_lines
f' = BC.unpack (B.drop 5 f) in
if t == f' || t == init f'
then return False
else do
scmd <- getSendmailCmd opts
if doHappyForwarding opts
then resendEmail t scmd m
else sendEmailDoc f' t "A forwarded darcs patch" cc
scmd (Just (empty,empty))
(packedString m')
return True
_ -> return False
cf (_:fs) cc = cf fs cc
is_from l = B.take 5 l == fromStart
fromStart :: B.ByteString
fromStart = BC.pack "From:"
\end{code}