-- Copyright (C) 2002-2004 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. {-# LANGUAGE CPP #-} module Darcs.UI.Commands.WhatsNew ( whatsnew , status ) where import Prelude () import Darcs.Prelude import Control.Monad ( void, when ) import Control.Monad.Reader ( runReaderT ) import Control.Monad.State ( evalStateT, liftIO ) import Darcs.Util.Tree ( Tree ) import System.Exit ( ExitCode (..), exitSuccess, exitWith ) import Data.List.Ordered ( nubSort ) import Darcs.Patch ( PrimOf, PrimPatch, RepoPatch , applyToTree, plainSummaryPrims, primIsHunk , listTouchedFiles, IsRepoType ) import Darcs.Patch.Apply ( ApplyState ) import Darcs.Patch.Choices ( patchChoicesLps, lpPatch ) import Darcs.Patch.FileHunk ( IsHunk (..) ) import Darcs.Patch.Format ( PatchListFormat (..) ) import Darcs.Patch.Inspect ( PatchInspect (..) ) import Darcs.Patch.Patchy ( Patchy ) import Darcs.Patch.Permutations ( partitionRL ) import Darcs.Patch.Prim.Class ( PrimDetails (..) ) import Darcs.Patch.Show ( ShowPatch ) import Darcs.Patch.Split ( primSplitter ) import Darcs.Patch.TouchesFiles ( choosePreTouching ) import Darcs.Patch.Witnesses.Ordered ( (:>) (..), FL (..), RL (..) , lengthFL, reverseFL, reverseRL ) import Darcs.Patch.Witnesses.Sealed ( Sealed (..), Sealed2 (..) , unFreeLeft ) import Darcs.Patch.Witnesses.Unsafe ( unsafeCoerceP ) import Darcs.Patch.Witnesses.WZipper ( FZipper (..) ) import Darcs.Repository ( RepoJob (..), Repository , listRegisteredFiles, readRecorded, readRepo , unrecordedChangesWithPatches, withRepository ) import Darcs.Repository.Diff ( treeDiff ) import Darcs.Repository.Prefs ( filetypeFunction ) import Darcs.Repository.State ( getMovesPs, getReplaces ) import Darcs.UI.Commands ( DarcsCommand(..), withStdOpts, amInRepository , commandAlias, nodefaults ) import Darcs.Repository.Resolution ( patchsetConflictResolutions ) import Darcs.UI.Commands.Util ( announceFiles, filterExistingPaths ) import Darcs.UI.Flags ( DarcsFlag (Summary, LookForAdds, LookForMoves), diffAlgorithm, diffingOpts , isUnified, useCache, fixSubPaths , verbosity, isInteractive, isUnified, lookForAdds, lookForMoves, lookForReplaces, hasSummary , scanKnown, useIndex ) import Darcs.UI.Options ( DarcsOption, (^), odesc, ocheck, onormalise, defaultFlags, parseFlags ) import qualified Darcs.UI.Options.All as O import Darcs.UI.PrintPatch ( contextualPrintPatch, printPatch , printPatchPager ) import Darcs.UI.SelectChanges ( InteractiveSelectionContext (..) , InteractiveSelectionM, KeyPress (..) , WhichChanges (..), backAll , backOne, currentFile , currentPatch, decide , decideWholeFile, helpFor , keysFor, prompt , selectionContextPrim, skipMundane , skipOne, printSummary ) import qualified Darcs.UI.SelectChanges as S ( PatchSelectionOptions (..) ) import Darcs.Util.Path ( AbsolutePath, SubPath, toFilePath, fp2fn ) import Darcs.Util.Printer ( putDocLn, renderString, RenderMode(..) , text, vcat ) import Darcs.Util.Prompt ( PromptConfig (..), promptChar ) whatsnewBasicOpts :: DarcsOption a (Maybe O.Summary -> O.WithContext -> Bool -> O.LookFor -> O.DiffAlgorithm -> Maybe String -> Maybe Bool -> a) whatsnewBasicOpts = O.summary ^ O.withContext ^ O.machineReadable ^ O.lookfor ^ O.diffAlgorithm ^ O.workingRepoDir ^ O.interactive -- False whatsnewAdvancedOpts :: DarcsOption a (O.UseIndex -> O.IncludeBoring -> a) whatsnewAdvancedOpts = O.useIndex ^ O.includeBoring whatsnewOpts :: DarcsOption a (Maybe O.Summary -> O.WithContext -> Bool -> O.LookFor -> O.DiffAlgorithm -> Maybe String -> Maybe Bool -> Maybe O.StdCmdAction -> Bool -> Bool -> O.Verbosity -> Bool -> O.UseIndex -> O.IncludeBoring -> O.UseCache -> Maybe String -> Bool -> Maybe String -> Bool -> a) whatsnewOpts = whatsnewBasicOpts `withStdOpts` whatsnewAdvancedOpts patchSelOpts :: [DarcsFlag] -> S.PatchSelectionOptions patchSelOpts flags = S.PatchSelectionOptions { S.verbosity = verbosity flags , S.matchFlags = [] , S.interactive = isInteractive True flags , S.selectDeps = O.PromptDeps -- option not supported, use default , S.summary = hasSummary (defaultSummary flags) flags , S.withContext = isUnified flags } defaultSummary :: [DarcsFlag] -> O.Summary defaultSummary flags | lookForAdds flags == O.YesLookForAdds = O.YesSummary | parseFlags O.machineReadable flags = O.YesSummary | otherwise = O.NoSummary whatsnew :: DarcsCommand [DarcsFlag] whatsnew = DarcsCommand { commandProgramName = "darcs" , commandName = "whatsnew" , commandHelp = whatsnewHelp , commandDescription = whatsnewDescription , commandExtraArgs = -1 , commandExtraArgHelp = ["[FILE or DIRECTORY]..."] , commandCommand = whatsnewCmd , commandPrereq = amInRepository , commandGetArgPossibilities = listRegisteredFiles , commandArgdefaults = nodefaults , commandAdvancedOptions = odesc whatsnewAdvancedOpts , commandBasicOptions = odesc whatsnewBasicOpts , commandDefaults = defaultFlags whatsnewOpts , commandCheckOptions = ocheck whatsnewOpts , commandParseOptions = onormalise whatsnewOpts } whatsnewDescription :: String whatsnewDescription = "List unrecorded changes in the working tree." whatsnewHelp :: String whatsnewHelp = "The `darcs whatsnew` command lists unrecorded changes to the working\n" ++ "tree. If you specify a set of files and directories, only unrecorded\n" ++ "changes to those files and directories are listed.\n" ++ "\n" ++ "With the `--summary` option, the changes are condensed to one line per\n" ++ "file, with mnemonics to indicate the nature and extent of the change.\n" ++ "The `--look-for-adds` option causes candidates for `darcs add` to be\n" ++ "included in the summary output. Summary mnemonics are as follows:\n" ++ "\n" ++ "* `A f` and `A d/` respectively mean an added file or directory.\n" ++ "* `R f` and `R d/` respectively mean a removed file or directory.\n" ++ "* `M f -N +M rP` means a modified file, with `N` lines deleted, `M`\n" ++ " lines added, and `P` lexical replacements.\n" ++ "* `f -> g` means a moved file or directory.\n" ++ "* `a f` and `a d/` respectively mean a new, but unadded, file or\n" ++ " directory, when using `--look-for-adds`.\n" ++ "\n" ++ " An exclamation mark (!) as in `R! foo.c`, means the hunk is known to\n" ++ " conflict with a hunk in another patch. The phrase `duplicated`\n" ++ " means the hunk is known to be identical to a hunk in another patch.\n" ++ "\n" ++ "The `--machine-readable` option implies `--summary` while making it more\n" ++ "parsable. Modified files are only shown as `M f`, and moves are shown in\n" ++ "two lines: `F f` and `T g` (as in 'From f To g').\n" ++ "\n" ++ "By default, `darcs whatsnew` uses Darcs' internal format for changes.\n" ++ "To see some context (unchanged lines) around each change, use the\n" ++ "`--unified` option. To view changes in conventional `diff` format, use\n" ++ "the `darcs diff` command; but note that `darcs whatsnew` is faster.\n" ++ "\n" ++ "This command exits unsuccessfully (returns a non-zero exit status) if\n" ++ "there are no unrecorded changes.\n" whatsnewCmd :: (AbsolutePath, AbsolutePath) -> [DarcsFlag] -> [String] -> IO () whatsnewCmd fps opts args = withRepository (useCache opts) $ RepoJob $ \(repo :: Repository rt p wR wU wR) -> do let scan = scanKnown (O.adds (parseFlags O.lookfor opts)) (parseFlags O.includeBoring opts) existing_files <- do files <- if null args then return Nothing else Just . nubSort <$> fixSubPaths fps args when (files == Just []) $ fail "No valid arguments were given." files' <- traverse (filterExistingPaths repo (verbosity opts) (useIndex opts) scan) files let files'' = fmap snd files' when (files'' == Just []) $ fail "None of the files you specified exist." return files'' let isLookForMoves = lookForMoves opts == O.YesLookForMoves && parseFlags O.summary opts /= Just O.NoSummary isLookForAdds = lookForAdds opts == O.YesLookForAdds && parseFlags O.summary opts /= Just O.NoSummary isLookForReplaces = lookForReplaces opts == O.YesLookForReplaces isMachineReadable = parseFlags O.machineReadable opts -- LookForAdds and LookForMoves implies Summary, unless it's explcitly disabled. opts' | isLookForAdds = (Summary : filter (\o -> LookForAdds /= o && LookForMoves /= o ) opts) | isMachineReadable = (Summary:opts) | otherwise = opts movesPs <- if isLookForMoves then getMovesPs repo existing_files else return NilFL Sealed replacePs <- if isLookForReplaces then getReplaces (diffingOpts opts) repo existing_files else return (Sealed NilFL) Sealed noLookChanges <- filteredUnrecordedChanges opts' repo existing_files movesPs (unsafeCoerceP replacePs :: FL (PrimOf p) wR wR) pristine <- readRecorded repo -- If we are looking for moves, return the corresponding FL of changes. -- If we are looking for adds, return the corresponding FL of changes. Sealed unaddedNewPathsPs <- if isLookForAdds then do -- Use opts not opts', here, since we *do* want to look for adds. Sealed lookChanges <- filteredUnrecordedChanges opts repo existing_files movesPs (unsafeCoerceP replacePs :: FL (PrimOf p) wR wR) noLookAddsTree <- applyAddPatchesToPristine noLookChanges pristine lookAddsTree <- applyAddPatchesToPristine lookChanges pristine ftf <- filetypeFunction -- Return the patches that create files/dirs that aren't yet added. unFreeLeft <$> treeDiff (diffAlgorithm opts) ftf noLookAddsTree lookAddsTree else return (Sealed NilFL) announceFiles (verbosity opts) existing_files "What's new in" exitOnNoChanges (unaddedNewPathsPs, noLookChanges) if maybeIsInteractive opts then runInteractive (interactiveHunks pristine) opts' pristine noLookChanges else do printChanges repo opts' pristine noLookChanges printUnaddedPaths unaddedNewPathsPs where -- |Filter out hunk patches (leaving add patches) and return the tree -- resulting from applying the filtered patches to the pristine tree. applyAddPatchesToPristine ps pristine = do adds :> _ <- return $ partitionRL primIsHunk $ reverseFL ps applyToTree (reverseRL adds) pristine exitOnNoChanges :: (FL p wX wY, FL p wU wV) -> IO () exitOnNoChanges (NilFL, NilFL) = do putStrLn "No changes!" exitWith $ ExitFailure 1 exitOnNoChanges _ = return () printUnaddedPaths :: PrimPatch p => FL p wX wY -> IO () printUnaddedPaths NilFL = return () printUnaddedPaths ps = putDocLn . lowercaseAs . renderString Encode . (plainSummaryPrims False []) $ ps -- Make any add markers lowercase, to distinguish new-but-unadded files -- from those that are unrecorded, but added. lowercaseAs x = vcat $ map (text . lowercaseA) $ lines x lowercaseA ('A' : x) = 'a' : x lowercaseA x = x -- |Appropriately print changes, according to the passed flags. printChanges :: forall rt p wR wU wX wY. (IsRepoType rt, RepoPatch p, ApplyState p ~ Tree, ApplyState (PrimOf p) ~ Tree) => Repository rt p wR wU wR -> [DarcsFlag] -> Tree IO -> FL (PrimOf p) wX wY -> IO () printChanges repo opts' pristine changes | Summary `elem` opts' = do r <- readRepo repo Sealed res <- return $ patchsetConflictResolutions r let conflictFns = map fp2fn $ nubSort $ listTouchedFiles res putDocLn $ plainSummaryPrims machineReadable conflictFns changes | isUnified opts' == O.YesContext = contextualPrintPatch pristine changes | otherwise = printPatch changes where machineReadable = parseFlags O.machineReadable opts -- |return the unrecorded changes that affect an optional list of paths. filteredUnrecordedChanges :: forall rt p wR wU wT. (RepoPatch p, ApplyState p ~ Tree, ApplyState (PrimOf p) ~ Tree) => [DarcsFlag] -> Repository rt p wR wU wT -> Maybe [SubPath] -> FL (PrimOf p) wR wT -- look-for-moves patches -> FL (PrimOf p) wT wT -- look-for-replaces patches -> IO (Sealed (FL (PrimOf p) wT)) filteredUnrecordedChanges opts' repo files movesPs replacesPs = let filePaths = map toFilePath <$> files in choosePreTouching filePaths <$> unrecordedChangesWithPatches movesPs replacesPs (diffingOpts opts') repo files -- | Runs the 'InteractiveSelectionM' code runInteractive :: (PatchListFormat p, IsHunk p, Patchy p, ShowPatch p, PrimPatch p, PatchInspect p, PrimDetails p, ApplyState p ~ Tree) => InteractiveSelectionM p wX wY () -- Selection to run -> [DarcsFlag] -- Command-line options -> Tree IO -- Pristine -> FL p wX wY -- A list of patches -> IO () runInteractive i opts pristine ps' = do let (choices',lps') = patchChoicesLps ps' let ps = evalStateT i $ ISC { total = lengthFL lps' , current = 0 , lps = FZipper NilRL lps' , choices = choices' } void $ runReaderT ps $ selectionContextPrim First "view" (patchSelOpts opts) (Just (primSplitter (diffAlgorithm opts))) Nothing (Just pristine) -- | The interactive part of @darcs whatsnew@ interactiveHunks :: (PatchListFormat p, IsHunk p, Patchy p, ShowPatch p, PatchInspect p, PrimDetails p, ApplyState p ~ Tree) => Tree IO -> InteractiveSelectionM p wX wY () interactiveHunks pristine = do c <- currentPatch case c of Nothing -> liftIO $ putStrLn "No more changes!" Just (Sealed2 lp) -> do liftIO $ printPatch (lpPatch lp) repeatThis lp where repeatThis lp = do thePrompt <- prompt -- "Shall I view this change? (n/m)" yorn <- liftIO $ promptChar (PromptConfig thePrompt (keysFor basic_options) (keysFor adv_options) (Just 'n') "?h") case yorn of -- View hunk in context 'v' -> liftIO (contextualPrintPatch pristine (lpPatch lp)) >> repeatThis lp -- View summary of the change 'x' -> liftIO (printSummary (lpPatch lp)) >> repeatThis lp -- View hunk and move on 'y' -> liftIO (contextualPrintPatch pristine (lpPatch lp)) >> decide True lp >> next_hunk -- Go to the next patch 'n' -> decide False lp >> next_hunk -- Skip the whole file 's' -> do currentFile >>= maybe (return ()) (\f -> decideWholeFile f False) next_hunk -- View hunk in a pager 'p' -> liftIO (printPatchPager $ lpPatch lp) >> repeatThis lp -- Next hunk 'j' -> next_hunk -- Previous hunk 'k' -> prev_hunk -- Start from the first change 'g' -> start_over -- Quit whatsnew 'q' -> liftIO $ exitSuccess _ -> do liftIO . putStrLn $ helpFor "whatsnew" basic_options adv_options repeatThis lp start_over = backAll >> interactiveHunks pristine next_hunk = skipOne >> skipMundane >> interactiveHunks pristine prev_hunk = backOne >> interactiveHunks pristine options_yn = [ KeyPress 'v' "view this hunk in a context" , KeyPress 'y' "view this hunk in a context and go to the next one" , KeyPress 'n' "go to the next hunk" ] optionsView = [ KeyPress 'p' "view this hunk in context wih pager " , KeyPress 'x' "view a summary of this patch" ] optionsNav = [ KeyPress 'q' "quit whatsnew" , KeyPress 's' "skip the rest of the changes to this file" , KeyPress 'j' "skip to the next hunk" , KeyPress 'k' "back up to previous hunk" , KeyPress 'g' "start over from the first hunk" ] basic_options = [ options_yn ] adv_options = [ optionsView, optionsNav ] -- |status is an alias for whatsnew, with implicit Summary and LookForAdds -- flags. We override the default description, to include the implicit flags. status :: DarcsCommand [DarcsFlag] status = statusAlias { commandCommand = statusCmd , commandDescription = statusDesc } where statusAlias = commandAlias "status" Nothing whatsnew statusCmd fps fs = commandCommand whatsnew fps (Summary : LookForAdds : fs) statusDesc = "Alias for `darcs " ++ commandName whatsnew ++ " -ls '." maybeIsInteractive :: [DarcsFlag] -> Bool maybeIsInteractive = maybe False id . parseFlags O.interactive