packages feed

darcs-2.3.0: src/Darcs/Gorsvet.hs

{-# LANGUAGE CPP, FlexibleInstances #-}
{-# OPTIONS_GHC -cpp -fglasgow-exts -fno-warn-orphans #-}

-- Copyright (C) 2009 Petr Rockai
--
-- Permission is hereby granted, free of charge, to any person
-- obtaining a copy of this software and associated documentation
-- files (the "Software"), to deal in the Software without
-- restriction, including without limitation the rights to use, copy,
-- modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be
-- included in all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-- BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-- ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.

#include "gadts.h"

module Darcs.Gorsvet where

import Prelude hiding ( all, filter, lines, read, readFile, writeFile )

-- darcs stuff
import ByteStringUtils( is_funky )
import Darcs.Repository ( Repository, slurp_pending )
import Darcs.Repository.Internal ( read_pending )
import Darcs.Patch ( RepoPatch, Prim, hunk, canonize, binary, apply
                   , sort_coalesceFL, addfile, rmfile, adddir, rmdir, invert)
import Darcs.Ordered ( FL(..), (+>+) )
import Darcs.Repository.Prefs ( filetype_function, FileType(..) )
import Darcs.IO
import Darcs.Sealed ( Sealed(Sealed), seal )
import Darcs.Patch( apply_to_filepaths )
import Darcs.Patch.Patchy ( Apply )
import Darcs.Patch.TouchesFiles ( choose_touching )
import Darcs.Patch.FileName ( fn2fp, FileName )

import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Char8 as BS
import Control.Monad.State.Strict
import System.Directory( removeFile, doesFileExist )
import Data.Maybe
import Data.List( union )

import Darcs.Arguments ( DarcsFlag( LookForAdds, IgnoreTimes ) )
import Darcs.RepoPath ( SubPath, sp2fn )

import Text.Regex( matchRegex )
import Darcs.Repository.Prefs( boring_regexps )

import Storage.Hashed
import Storage.Hashed.Tree
import qualified Storage.Hashed.Index as I
import Storage.Hashed.AnchoredPath
import Storage.Hashed.Darcs( darcsFormatHash, darcsTreeHash )
import Storage.Hashed.Monad
    ( virtualTreeIO, hashedTreeIO, plainTreeIO
    , unlink, rename, createDirectory, writeFile
    , readFile -- ratify readFile: haskell_policy je natvrdlá
    , cwd, tree, TreeIO )
import Storage.Hashed

floatFn :: FileName -> AnchoredPath
floatFn = floatPath . fn2fp

instance ReadableDirectory TreeIO where
    mDoesDirectoryExist d = gets (\x -> isJust $ findTree (tree x) (floatFn d))
    mDoesFileExist f = gets (\x -> isJust $ findFile (tree x) (floatFn f))
    mInCurrentDirectory d action = do -- TODO bracket?
      wd <- gets cwd
      modify (\x -> x { cwd = floatFn d })
      x <- action
      modify (\x' -> x' { cwd = wd })
      return x
    mGetDirectoryContents = error "get dir contents"
    mReadFilePS p = do x <- readFile (floatFn p) -- ratify readFile: ...
                       return $ BS.concat (BL.toChunks x)

instance WriteableDirectory TreeIO where
    mWithCurrentDirectory = mInCurrentDirectory
    mSetFileExecutable _ _ = return ()
    mWriteFilePS p ps = writeFile -- ratify readFile: haskell_policy is stupid.
          (floatFn p) (BL.fromChunks [ps])
    mCreateDirectory p = createDirectory (floatFn p)
    mRename from to = rename (floatFn from) (floatFn to)
    mRemoveDirectory = unlink . floatFn
    mRemoveFile = unlink . floatFn

treeDiff :: (FilePath -> FileType) -> Tree -> Tree -> IO (FL Prim C(x y))
#ifdef GADT_WITNESSES
treeDiff = undefined -- Sigh.
#else
treeDiff ft t1 t2 = do
  (from, to) <- diffTrees t1 t2
  diffs <- sequence $ zipTrees diff from to
  return $ foldr (+>+) NilFL diffs
    where diff :: AnchoredPath -> Maybe TreeItem -> Maybe TreeItem
               -> IO (FL Prim)
          diff _ (Just (SubTree _)) (Just (SubTree _)) = return NilFL
          diff p (Just (SubTree _)) Nothing =
              return $ rmdir (anchorPath "" p) :>: NilFL
          diff p Nothing (Just (SubTree _)) =
              return $ adddir (anchorPath "" p) :>: NilFL
          diff p Nothing b'@(Just (File _)) =
              do diff' <- diff p (Just (File emptyBlob)) b'
                 return $ addfile (anchorPath "" p) :>: diff'
          diff p a'@(Just (File _)) Nothing =
              do diff' <- diff p a' (Just (File emptyBlob))
                 return $ diff' +>+ (rmfile (anchorPath "" p) :>: NilFL)
          diff p (Just (File a')) (Just (File b')) =
              do a <- read a'
                 b <- read b'
                 let path = anchorPath "" p
                 case ft path of
                   TextFile | no_bin a && no_bin b ->
                                return $ text_diff path a b
                   _ -> return $ if a /= b
                                    then binary path (strict a) (strict b) :>: NilFL
                                    else NilFL
          diff p _ _ = fail $ "Missing case at path " ++ show p
          text_diff p a b
              | BL.null a && BL.null b = NilFL
              | BL.null a = diff_from_empty p b
              | BL.null b = diff_to_empty p a
              | otherwise = line_diff p (lines a) (lines b)
          line_diff p a b = canonize (hunk p 1 a b)
          diff_to_empty p x | BL.last x == '\n' = line_diff p (init $ lines x) []
                            | otherwise = line_diff p (lines x) [BS.empty]
          diff_from_empty p x = invert (diff_to_empty p x)
          no_bin = not . is_funky . strict . BL.take 4096
          lines = map strict . BL.split '\n'
          strict = BS.concat . BL.toChunks
#endif

readRecorded :: (RepoPatch p) => Repository p C(r u t) -> IO Tree
readRecorded _ = readDarcsPristine "."

readRecordedAndPending :: (RepoPatch p) => Repository p C(r u t) -> IO Tree
readRecordedAndPending repo = do
  pristine <- readRecorded repo
  Sealed pending <- pendingChanges repo []
  applyToTree pending pristine

pendingChanges :: (RepoPatch p) => Repository p C(r u t)
               -> [SubPath] -> IO (Sealed (FL Prim C(r)))
pendingChanges repo paths = do
  slurp_pending repo -- XXX: only here to get us the "pending conflicts" check
                     -- that I don't know yet how to implement properly
  Sealed pending <- read_pending repo
  let files = map (fn2fp . sp2fn) paths
      pre_files = apply_to_filepaths (invert pending) files
      relevant = case paths of
                   [] -> seal pending
                   _ -> choose_touching pre_files pending
  return relevant

applyToTree :: (Apply p) => p C(x y) -> Tree -> IO Tree
applyToTree patch t = snd `fmap` virtualTreeIO (apply [] patch) t

unrecordedChanges :: (RepoPatch p) => [DarcsFlag] -> Repository p C(r u t)
                  -> [SubPath] -> IO (FL Prim C(r y))
unrecordedChanges opts repo paths = do
  pristine <- readDarcsPristine "."
  Sealed pending <- pendingChanges repo paths
  (_, current') <- virtualTreeIO (apply [] pending) pristine
  relevant <- restrictSubpaths repo paths
  nonboring <- restrictBoring

  let current = relevant current'
  working <- case (LookForAdds `elem` opts, IgnoreTimes `elem` opts) of
               (False, False) -> do
                 all <- readIndex repo
                 expand (relevant all)
               (False, True) -> do
                 guide <- expand current
                 all <- readPlainTree "."
                 return $ relevant $ (restrict guide) all
               -- TODO (True, False) could use a more efficient implementation...
               (True, _) -> do
                 all <- readPlainTree "."
                 return $ relevant $ nonboring all

  ft <- filetype_function
  diff <- treeDiff ft current working
  return $ sort_coalesceFL (pending +>+ diff)

applyToTentativePristine :: (Apply p) => t -> p C(x y) -> IO ()
applyToTentativePristine _ patches =
    do pristine <- readDarcsPristine "."
       (_, t) <- hashedTreeIO (apply [] patches)
                 pristine "_darcs/pristine.hashed"
       BS.writeFile "_darcs/tentative_pristine" $
         BS.concat [BS.pack "pristine:"
                   , darcsFormatHash (fromJust $ treeHash t)]

applyToWorking :: (RepoPatch p) => Repository p C(r u t)
               -> Sealed (FL Prim C(u)) -> IO Tree
applyToWorking repo (Sealed patches) =
    do working <- readIndex repo
       snd `fmap` plainTreeIO (apply [] patches) working "."

filter_paths :: [AnchoredPath] -> AnchoredPath -> t -> Bool
filter_paths files =
    \p _ -> any (\x -> x `isPrefix` p || p `isPrefix` x) files

restrict_paths :: [AnchoredPath] -> Tree -> Tree
restrict_paths files = if null files
                          then id
                          else filter $ filter_paths files

restrict_subpaths :: [SubPath] -> Tree -> Tree
restrict_subpaths = restrict_paths . map (floatPath . fn2fp . sp2fn)

restrictSubpaths :: (RepoPatch p) => Repository p C(r u t) -> [SubPath] -> IO (Tree -> Tree)
restrictSubpaths repo subpaths = do
  Sealed pending <- read_pending repo
  let paths = map (fn2fp . sp2fn) subpaths
      paths' = paths `union` apply_to_filepaths pending paths
      anchored = map floatPath paths'
  return $ restrict_paths anchored

restrictBoring :: IO (Tree -> Tree)
restrictBoring = do
  boring <- boring_regexps
  let boring' (AnchoredPath (Name x:_)) _ | x == BS.pack "_darcs" = False
      boring' p _ = not $ any (\rx -> isJust $ matchRegex rx p') boring
          where p' = anchorPath "" p
  return $ filter boring'

readIndex :: (RepoPatch p) => Repository p C(r u t) -> IO Tree
readIndex repo = do
  invalid <- doesFileExist "_darcs/index_invalid"
  exist <- doesFileExist "_darcs/index"
  format_valid <- if exist
                     then I.indexFormatValid "_darcs/index"
                     else return True
  when (exist && not format_valid) $ removeFile "_darcs/index"
  if (not exist || invalid || not format_valid)
     then do pris <- readRecordedAndPending repo
             idx <- I.updateIndexFrom "_darcs/index" darcsTreeHash pris
             when invalid $ removeFile "_darcs/index_invalid"
             return idx
     else I.readIndex "_darcs/index" darcsTreeHash

invalidateIndex :: t -> IO ()
invalidateIndex _ = do
  BS.writeFile "_darcs/index_invalid" BS.empty