Compare commits
No commits in common. "ci" and "tweak-fetch" have entirely different histories.
ci
...
tweak-fetc
852 changed files with 29652 additions and 153 deletions
|
@ -1,18 +0,0 @@
|
||||||
Support ghc-9.8 by widening a lot of constraints.
|
|
||||||
|
|
||||||
This patch can be removed once upstream supports ghc 9.8 offically.
|
|
||||||
|
|
||||||
diff -uprN git-annex-10.20240227.orig/cabal.project git-annex-10.20240227/cabal.project
|
|
||||||
--- git-annex-10.20240227.orig/cabal.project 1970-01-01 01:00:00.000000000 +0100
|
|
||||||
+++ git-annex-10.20240227/cabal.project 2024-04-28 13:30:14.061706299 +0200
|
|
||||||
@@ -0,0 +1,10 @@
|
|
||||||
+packages: *.cabal
|
|
||||||
+
|
|
||||||
+allow-newer: dav
|
|
||||||
+allow-newer: haskeline:filepath
|
|
||||||
+allow-newer: haskeline:directory
|
|
||||||
+allow-newer: xml-hamlet
|
|
||||||
+allow-newer: aws:filepath
|
|
||||||
+allow-newer: dbus:network
|
|
||||||
+allow-newer: dbus:filepath
|
|
||||||
+allow-newer: microstache:filepath
|
|
|
@ -1,85 +0,0 @@
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
ref_name:
|
|
||||||
description: 'Tag or commit'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cabal-config-edge:
|
|
||||||
name: Generate cabal config for edge
|
|
||||||
runs-on: x86_64
|
|
||||||
container:
|
|
||||||
image: alpine:edge
|
|
||||||
env:
|
|
||||||
CI_ALPINE_TARGET_RELEASE: edge
|
|
||||||
steps:
|
|
||||||
- name: Environment setup
|
|
||||||
run: apk add nodejs git cabal patch
|
|
||||||
- name: Repo pull
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
ref: ${{ inputs.ref_name }}
|
|
||||||
- name: Config generation
|
|
||||||
run: |
|
|
||||||
patch -p1 -i .forgejo/patches/ghc-9.8.patch
|
|
||||||
HOME="${{ github.workspace}}"/cabal_cache cabal update
|
|
||||||
HOME="${{ github.workspace}}"/cabal_cache cabal v2-freeze --shadow-installed-packages --strong-flags --flags="+assistant +webapp +pairing +production +torrentparser +magicmime +benchmark -debuglocks +dbus +networkbsd +gitlfs +httpclientrestricted"
|
|
||||||
mv cabal.project.freeze git-annex.config
|
|
||||||
- name: Package upload
|
|
||||||
uses: forgejo/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: cabalconfigedge
|
|
||||||
path: git-annex*.config
|
|
||||||
cabal-config-v321:
|
|
||||||
name: Generate cabal config for v3.21
|
|
||||||
runs-on: x86_64
|
|
||||||
container:
|
|
||||||
image: alpine:3.21
|
|
||||||
env:
|
|
||||||
CI_ALPINE_TARGET_RELEASE: v3.21
|
|
||||||
steps:
|
|
||||||
- name: Environment setup
|
|
||||||
run: apk add nodejs git cabal patch
|
|
||||||
- name: Repo pull
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
ref: ${{ inputs.ref_name }}
|
|
||||||
- name: Config generation
|
|
||||||
run: |
|
|
||||||
patch -p1 -i .forgejo/patches/ghc-9.8.patch
|
|
||||||
HOME="${{ github.workspace }}"/cabal_cache cabal update
|
|
||||||
HOME="${{ github.workspace }}"/cabal_cache cabal v2-freeze --shadow-installed-packages --strong-flags --flags="+assistant +webapp +pairing +production +torrentparser +magicmime +benchmark -debuglocks +dbus +networkbsd +gitlfs +httpclientrestricted"
|
|
||||||
mv cabal.project.freeze git-annex.config
|
|
||||||
- name: Package upload
|
|
||||||
uses: forgejo/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: cabalconfig321
|
|
||||||
path: git-annex*.config
|
|
||||||
upload-tarball:
|
|
||||||
name: Upload to generic repo
|
|
||||||
runs-on: x86_64
|
|
||||||
needs: [cabal-config-edge,cabal-config-v321]
|
|
||||||
container:
|
|
||||||
image: alpine:latest
|
|
||||||
steps:
|
|
||||||
- name: Environment setup
|
|
||||||
run: apk add nodejs curl findutils
|
|
||||||
- name: Package download
|
|
||||||
uses: forgejo/download-artifact@v3
|
|
||||||
- name: Package deployment
|
|
||||||
run: |
|
|
||||||
if test $GITHUB_REF_NAME == "ci" ; then
|
|
||||||
CI_REF_NAME=${{ inputs.ref_name }}
|
|
||||||
else
|
|
||||||
CI_REF_NAME=$GITHUB_REF_NAME
|
|
||||||
fi
|
|
||||||
curl --user ${{ vars.CODE_FORGEJO_USER }}:${{ secrets.CODE_FORGEJO_TOKEN }} --upload-file ./cabalconfigedge/git-annex.config ${{ github.server_url }}/api/packages/mirrors/generic/git-annex/$CI_REF_NAME/git-annex-$CI_REF_NAME-edge.cabal
|
|
||||||
curl --user ${{ vars.CODE_FORGEJO_USER }}:${{ secrets.CODE_FORGEJO_TOKEN }} --upload-file ./cabalconfig321/git-annex.config ${{ github.server_url }}/api/packages/mirrors/generic/git-annex/$CI_REF_NAME/git-annex-$CI_REF_NAME-v321.cabal
|
|
|
@ -1,50 +0,0 @@
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
schedule:
|
|
||||||
- cron: '@hourly'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
mirror:
|
|
||||||
name: Pull from upstream
|
|
||||||
runs-on: x86_64
|
|
||||||
container:
|
|
||||||
image: alpine:latest
|
|
||||||
env:
|
|
||||||
upstream: https://git.joeyh.name/git/git-annex.git
|
|
||||||
tags: '10.2025*'
|
|
||||||
steps:
|
|
||||||
- name: Environment setup
|
|
||||||
run: apk add grep git sed coreutils bash nodejs
|
|
||||||
- name: Fetch destination
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch_depth: 1
|
|
||||||
ref: ci
|
|
||||||
token: ${{ secrets.CODE_FORGEJO_TOKEN }}
|
|
||||||
- name: Missing tag detecting
|
|
||||||
run: |
|
|
||||||
git ls-remote $upstream "refs/tags/$tags" | grep -v '{' | sed 's|.*/||' | sort > upstream_tags
|
|
||||||
git ls-remote ${{ github.server_url}}/${{ github.repository }} "refs/tags/$tags" | grep -v '{' | sed 's|.*/||' | sort > destination_tags
|
|
||||||
comm -23 upstream_tags destination_tags > missing_tags
|
|
||||||
echo "Missing tags:"
|
|
||||||
cat missing_tags
|
|
||||||
- name: Missing tag fetch
|
|
||||||
run: |
|
|
||||||
git remote add upstream $upstream
|
|
||||||
while read tag; do
|
|
||||||
git fetch upstream tag $tag --no-tags
|
|
||||||
done < missing_tags
|
|
||||||
- name: Packaging workflow injection
|
|
||||||
run: |
|
|
||||||
while read tag; do
|
|
||||||
git checkout $tag
|
|
||||||
git tag -d $tag
|
|
||||||
git checkout ci -- ./.forgejo
|
|
||||||
git config user.name "forgejo-actions[bot]"
|
|
||||||
git config user.email "dev@ayakael.net"
|
|
||||||
git commit -m 'Inject custom workflow'
|
|
||||||
git tag -a $tag -m $tag
|
|
||||||
done < missing_tags
|
|
||||||
- name: Push to destination
|
|
||||||
run: git push --force origin refs/tags/*:refs/tags/* --tags
|
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
debian/changelog merge=dpkg-mergechangelogs
|
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
*.hi
|
||||||
|
*.o
|
||||||
|
test
|
||||||
|
configure
|
||||||
|
Build/SysConfig.hs
|
||||||
|
git-annex
|
||||||
|
git-annex-shell
|
||||||
|
git-union-merge
|
||||||
|
git-annex.1
|
||||||
|
git-annex-shell.1
|
||||||
|
git-union-merge.1
|
||||||
|
doc/.ikiwiki
|
||||||
|
html
|
||||||
|
*.tix
|
||||||
|
.hpc
|
||||||
|
Utility/Touch.hs
|
||||||
|
Utility/StatFS.hs
|
||||||
|
Remote/S3.hs
|
||||||
|
dist
|
134
Annex.hs
Normal file
134
Annex.hs
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
{- git-annex monad
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
|
module Annex (
|
||||||
|
Annex,
|
||||||
|
AnnexState(..),
|
||||||
|
OutputType(..),
|
||||||
|
new,
|
||||||
|
newState,
|
||||||
|
run,
|
||||||
|
eval,
|
||||||
|
getState,
|
||||||
|
changeState,
|
||||||
|
gitRepo,
|
||||||
|
inRepo,
|
||||||
|
fromRepo,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad.IO.Control
|
||||||
|
import Control.Monad.State
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Config
|
||||||
|
import Git.CatFile
|
||||||
|
import qualified Git.Queue
|
||||||
|
import Types.Backend
|
||||||
|
import qualified Types.Remote
|
||||||
|
import Types.Crypto
|
||||||
|
import Types.BranchState
|
||||||
|
import Types.TrustLevel
|
||||||
|
import Types.UUID
|
||||||
|
import qualified Utility.Matcher
|
||||||
|
import qualified Utility.Format
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
-- git-annex's monad
|
||||||
|
newtype Annex a = Annex { runAnnex :: StateT AnnexState IO a }
|
||||||
|
deriving (
|
||||||
|
Monad,
|
||||||
|
MonadIO,
|
||||||
|
MonadControlIO,
|
||||||
|
MonadState AnnexState,
|
||||||
|
Functor,
|
||||||
|
Applicative
|
||||||
|
)
|
||||||
|
|
||||||
|
data OutputType = NormalOutput | QuietOutput | JSONOutput
|
||||||
|
|
||||||
|
-- internal state storage
|
||||||
|
data AnnexState = AnnexState
|
||||||
|
{ repo :: Git.Repo
|
||||||
|
, backends :: [Backend Annex]
|
||||||
|
, remotes :: [Types.Remote.Remote Annex]
|
||||||
|
, repoqueue :: Git.Queue.Queue
|
||||||
|
, output :: OutputType
|
||||||
|
, force :: Bool
|
||||||
|
, fast :: Bool
|
||||||
|
, auto :: Bool
|
||||||
|
, format :: Maybe Utility.Format.Format
|
||||||
|
, branchstate :: BranchState
|
||||||
|
, catfilehandle :: Maybe CatFileHandle
|
||||||
|
, forcebackend :: Maybe String
|
||||||
|
, forcenumcopies :: Maybe Int
|
||||||
|
, toremote :: Maybe String
|
||||||
|
, fromremote :: Maybe String
|
||||||
|
, limit :: Either [Utility.Matcher.Token (FilePath -> Annex Bool)] (Utility.Matcher.Matcher (FilePath -> Annex Bool))
|
||||||
|
, forcetrust :: [(UUID, TrustLevel)]
|
||||||
|
, trustmap :: Maybe TrustMap
|
||||||
|
, ciphers :: M.Map EncryptedCipher Cipher
|
||||||
|
}
|
||||||
|
|
||||||
|
newState :: Git.Repo -> AnnexState
|
||||||
|
newState gitrepo = AnnexState
|
||||||
|
{ repo = gitrepo
|
||||||
|
, backends = []
|
||||||
|
, remotes = []
|
||||||
|
, repoqueue = Git.Queue.new
|
||||||
|
, output = NormalOutput
|
||||||
|
, force = False
|
||||||
|
, fast = False
|
||||||
|
, auto = False
|
||||||
|
, format = Nothing
|
||||||
|
, branchstate = startBranchState
|
||||||
|
, catfilehandle = Nothing
|
||||||
|
, forcebackend = Nothing
|
||||||
|
, forcenumcopies = Nothing
|
||||||
|
, toremote = Nothing
|
||||||
|
, fromremote = Nothing
|
||||||
|
, limit = Left []
|
||||||
|
, forcetrust = []
|
||||||
|
, trustmap = Nothing
|
||||||
|
, ciphers = M.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
{- Create and returns an Annex state object for the specified git repo. -}
|
||||||
|
new :: Git.Repo -> IO AnnexState
|
||||||
|
new gitrepo = newState <$> Git.Config.read gitrepo
|
||||||
|
|
||||||
|
{- performs an action in the Annex monad -}
|
||||||
|
run :: AnnexState -> Annex a -> IO (a, AnnexState)
|
||||||
|
run s a = runStateT (runAnnex a) s
|
||||||
|
eval :: AnnexState -> Annex a -> IO a
|
||||||
|
eval s a = evalStateT (runAnnex a) s
|
||||||
|
|
||||||
|
{- Gets a value from the internal state, selected by the passed value
|
||||||
|
- constructor. -}
|
||||||
|
getState :: (AnnexState -> a) -> Annex a
|
||||||
|
getState = gets
|
||||||
|
|
||||||
|
{- Applies a state mutation function to change the internal state.
|
||||||
|
-
|
||||||
|
- Example: changeState $ \s -> s { output = QuietOutput }
|
||||||
|
-}
|
||||||
|
changeState :: (AnnexState -> AnnexState) -> Annex ()
|
||||||
|
changeState = modify
|
||||||
|
|
||||||
|
{- Returns the annex's git repository. -}
|
||||||
|
gitRepo :: Annex Git.Repo
|
||||||
|
gitRepo = getState repo
|
||||||
|
|
||||||
|
{- Runs an IO action in the annex's git repository. -}
|
||||||
|
inRepo :: (Git.Repo -> IO a) -> Annex a
|
||||||
|
inRepo a = liftIO . a =<< gitRepo
|
||||||
|
|
||||||
|
{- Extracts a value from the annex's git repisitory. -}
|
||||||
|
fromRepo :: (Git.Repo -> a) -> Annex a
|
||||||
|
fromRepo a = a <$> gitRepo
|
312
Annex/Branch.hs
Normal file
312
Annex/Branch.hs
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
{- management of the git-annex branch
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.Branch (
|
||||||
|
name,
|
||||||
|
hasOrigin,
|
||||||
|
hasSibling,
|
||||||
|
create,
|
||||||
|
update,
|
||||||
|
updateTo,
|
||||||
|
get,
|
||||||
|
change,
|
||||||
|
commit,
|
||||||
|
files,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Annex.Exception
|
||||||
|
import Annex.BranchState
|
||||||
|
import Annex.Journal
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Command
|
||||||
|
import qualified Git.Ref
|
||||||
|
import qualified Git.Branch
|
||||||
|
import qualified Git.UnionMerge
|
||||||
|
import qualified Git.HashObject
|
||||||
|
import qualified Git.Index
|
||||||
|
import Annex.CatFile
|
||||||
|
|
||||||
|
{- Name of the branch that is used to store git-annex's information. -}
|
||||||
|
name :: Git.Ref
|
||||||
|
name = Git.Ref "git-annex"
|
||||||
|
|
||||||
|
{- Fully qualified name of the branch. -}
|
||||||
|
fullname :: Git.Ref
|
||||||
|
fullname = Git.Ref $ "refs/heads/" ++ show name
|
||||||
|
|
||||||
|
{- Branch's name in origin. -}
|
||||||
|
originname :: Git.Ref
|
||||||
|
originname = Git.Ref $ "origin/" ++ show name
|
||||||
|
|
||||||
|
{- Does origin/git-annex exist? -}
|
||||||
|
hasOrigin :: Annex Bool
|
||||||
|
hasOrigin = inRepo $ Git.Ref.exists originname
|
||||||
|
|
||||||
|
{- Does the git-annex branch or a sibling foo/git-annex branch exist? -}
|
||||||
|
hasSibling :: Annex Bool
|
||||||
|
hasSibling = not . null <$> siblingBranches
|
||||||
|
|
||||||
|
{- List of git-annex (refs, branches), including the main one and any
|
||||||
|
- from remotes. Duplicate refs are filtered out. -}
|
||||||
|
siblingBranches :: Annex [(Git.Ref, Git.Branch)]
|
||||||
|
siblingBranches = inRepo $ Git.Ref.matching name
|
||||||
|
|
||||||
|
{- Creates the branch, if it does not already exist. -}
|
||||||
|
create :: Annex ()
|
||||||
|
create = do
|
||||||
|
_ <- getBranch
|
||||||
|
return ()
|
||||||
|
|
||||||
|
{- Returns the ref of the branch, creating it first if necessary. -}
|
||||||
|
getBranch :: Annex (Git.Ref)
|
||||||
|
getBranch = maybe (hasOrigin >>= go >>= use) (return) =<< branchsha
|
||||||
|
where
|
||||||
|
go True = do
|
||||||
|
inRepo $ Git.Command.run "branch"
|
||||||
|
[Param $ show name, Param $ show originname]
|
||||||
|
fromMaybe (error $ "failed to create " ++ show name)
|
||||||
|
<$> branchsha
|
||||||
|
go False = withIndex' True $ do
|
||||||
|
inRepo $ Git.Branch.commit "branch created" fullname []
|
||||||
|
use sha = do
|
||||||
|
setIndexSha sha
|
||||||
|
return sha
|
||||||
|
branchsha = inRepo $ Git.Ref.sha fullname
|
||||||
|
|
||||||
|
{- Ensures that the branch and index are is up-to-date; should be
|
||||||
|
- called before data is read from it. Runs only once per git-annex run.
|
||||||
|
-}
|
||||||
|
update :: Annex ()
|
||||||
|
update = runUpdateOnce $ updateTo =<< siblingBranches
|
||||||
|
|
||||||
|
{- Merges the specified Refs into the index, if they have any changes not
|
||||||
|
- already in it. The Branch names are only used in the commit message;
|
||||||
|
- it's even possible that the provided Branches have not been updated to
|
||||||
|
- point to the Refs yet.
|
||||||
|
-
|
||||||
|
- Before refs are merged into the index, it's important to first stage the
|
||||||
|
- journal into the index. Otherwise, any changes in the journal would
|
||||||
|
- later get staged, and might overwrite changes made during the merge.
|
||||||
|
- If no Refs are provided, the journal is still staged and committed.
|
||||||
|
-
|
||||||
|
- (It would be cleaner to handle the merge by updating the journal, not the
|
||||||
|
- index, with changes from the branches.)
|
||||||
|
-
|
||||||
|
- The branch is fast-forwarded if possible, otherwise a merge commit is
|
||||||
|
- made.
|
||||||
|
-}
|
||||||
|
updateTo :: [(Git.Ref, Git.Branch)] -> Annex ()
|
||||||
|
updateTo pairs = do
|
||||||
|
-- ensure branch exists, and get its current ref
|
||||||
|
branchref <- getBranch
|
||||||
|
-- check what needs updating before taking the lock
|
||||||
|
dirty <- journalDirty
|
||||||
|
(refs, branches) <- unzip <$> filterM isnewer pairs
|
||||||
|
if (not dirty && null refs)
|
||||||
|
then updateIndex branchref
|
||||||
|
else withIndex $ lockJournal $ do
|
||||||
|
when dirty stageJournal
|
||||||
|
let merge_desc = if null branches
|
||||||
|
then "update"
|
||||||
|
else "merging " ++
|
||||||
|
unwords (map Git.Ref.describe branches) ++
|
||||||
|
" into " ++ show name
|
||||||
|
unless (null branches) $ do
|
||||||
|
showSideAction merge_desc
|
||||||
|
mergeIndex refs
|
||||||
|
ff <- if dirty
|
||||||
|
then return False
|
||||||
|
else inRepo $ Git.Branch.fastForward fullname refs
|
||||||
|
if ff
|
||||||
|
then updateIndex branchref
|
||||||
|
else commitBranch branchref merge_desc
|
||||||
|
(nub $ fullname:refs)
|
||||||
|
invalidateCache
|
||||||
|
where
|
||||||
|
isnewer (r, _) = inRepo $ Git.Branch.changed fullname r
|
||||||
|
|
||||||
|
{- Gets the content of a file on the branch, or content from the journal, or
|
||||||
|
- staged in the index.
|
||||||
|
-
|
||||||
|
- Returns an empty string if the file doesn't exist yet. -}
|
||||||
|
get :: FilePath -> Annex String
|
||||||
|
get = get' False
|
||||||
|
|
||||||
|
{- Like get, but does not merge the branch, so the info returned may not
|
||||||
|
- reflect changes in remotes. (Changing the value this returns, and then
|
||||||
|
- merging is always the same as using get, and then changing its value.) -}
|
||||||
|
getStale :: FilePath -> Annex String
|
||||||
|
getStale = get' True
|
||||||
|
|
||||||
|
get' :: Bool -> FilePath -> Annex String
|
||||||
|
get' staleok file = fromcache =<< getCache file
|
||||||
|
where
|
||||||
|
fromcache (Just content) = return content
|
||||||
|
fromcache Nothing = fromjournal =<< getJournalFile file
|
||||||
|
fromjournal (Just content) = cache content
|
||||||
|
fromjournal Nothing
|
||||||
|
| staleok = withIndex frombranch
|
||||||
|
| otherwise = withIndexUpdate $ frombranch >>= cache
|
||||||
|
frombranch = L.unpack <$> catFile fullname file
|
||||||
|
cache content = do
|
||||||
|
setCache file content
|
||||||
|
return content
|
||||||
|
|
||||||
|
{- Applies a function to modifiy the content of a file.
|
||||||
|
-
|
||||||
|
- Note that this does not cause the branch to be merged, it only
|
||||||
|
- modifes the current content of the file on the branch.
|
||||||
|
-}
|
||||||
|
change :: FilePath -> (String -> String) -> Annex ()
|
||||||
|
change file a = lockJournal $ getStale file >>= return . a >>= set file
|
||||||
|
|
||||||
|
{- Records new content of a file into the journal and cache. -}
|
||||||
|
set :: FilePath -> String -> Annex ()
|
||||||
|
set file content = do
|
||||||
|
setJournalFile file content
|
||||||
|
setCache file content
|
||||||
|
|
||||||
|
{- Stages the journal, and commits staged changes to the branch. -}
|
||||||
|
commit :: String -> Annex ()
|
||||||
|
commit message = whenM journalDirty $ lockJournal $ do
|
||||||
|
stageJournal
|
||||||
|
ref <- getBranch
|
||||||
|
withIndex $ commitBranch ref message [fullname]
|
||||||
|
|
||||||
|
{- Commits the staged changes in the index to the branch.
|
||||||
|
-
|
||||||
|
- Ensures that the branch's index file is first updated to the state
|
||||||
|
- of the brannch at branchref, before running the commit action. This
|
||||||
|
- is needed because the branch may have had changes pushed to it, that
|
||||||
|
- are not yet reflected in the index.
|
||||||
|
-
|
||||||
|
- Also safely handles a race that can occur if a change is being pushed
|
||||||
|
- into the branch at the same time. When the race happens, the commit will
|
||||||
|
- be made on top of the newly pushed change, but without the index file
|
||||||
|
- being updated to include it. The result is that the newly pushed
|
||||||
|
- change is reverted. This race is detected and another commit made
|
||||||
|
- to fix it.
|
||||||
|
-
|
||||||
|
- The branchref value can have been obtained using getBranch at any
|
||||||
|
- previous point, though getting it a long time ago makes the race
|
||||||
|
- more likely to occur.
|
||||||
|
-}
|
||||||
|
commitBranch :: Git.Ref -> String -> [Git.Ref] -> Annex ()
|
||||||
|
commitBranch branchref message parents = do
|
||||||
|
updateIndex branchref
|
||||||
|
committedref <- inRepo $ Git.Branch.commit message fullname parents
|
||||||
|
setIndexSha committedref
|
||||||
|
parentrefs <- commitparents <$> catObject committedref
|
||||||
|
when (racedetected branchref parentrefs) $
|
||||||
|
fixrace committedref parentrefs
|
||||||
|
where
|
||||||
|
-- look for "parent ref" lines and return the refs
|
||||||
|
commitparents = map (Git.Ref . snd) . filter isparent .
|
||||||
|
map (toassoc . L.unpack) . L.lines
|
||||||
|
toassoc = separate (== ' ')
|
||||||
|
isparent (k,_) = k == "parent"
|
||||||
|
|
||||||
|
{- The race can be detected by checking the commit's
|
||||||
|
- parent, which will be the newly pushed branch,
|
||||||
|
- instead of the expected ref that the index was updated to. -}
|
||||||
|
racedetected expectedref parentrefs
|
||||||
|
| expectedref `elem` parentrefs = False -- good parent
|
||||||
|
| otherwise = True -- race!
|
||||||
|
|
||||||
|
{- To recover from the race, union merge the lost refs
|
||||||
|
- into the index, and recommit on top of the bad commit. -}
|
||||||
|
fixrace committedref lostrefs = do
|
||||||
|
mergeIndex lostrefs
|
||||||
|
commitBranch committedref racemessage [committedref]
|
||||||
|
|
||||||
|
racemessage = message ++ " (recovery from race)"
|
||||||
|
|
||||||
|
{- Lists all files on the branch. There may be duplicates in the list. -}
|
||||||
|
files :: Annex [FilePath]
|
||||||
|
files = withIndexUpdate $ do
|
||||||
|
bfiles <- inRepo $ Git.Command.pipeNullSplit
|
||||||
|
[Params "ls-tree --name-only -r -z", Param $ show fullname]
|
||||||
|
jfiles <- getJournalledFiles
|
||||||
|
return $ jfiles ++ bfiles
|
||||||
|
|
||||||
|
|
||||||
|
{- Populates the branch's index file with the current branch contents.
|
||||||
|
-
|
||||||
|
- This is only done when the index doesn't yet exist, and the index
|
||||||
|
- is used to build up changes to be commited to the branch, and merge
|
||||||
|
- in changes from other branches.
|
||||||
|
-}
|
||||||
|
genIndex :: Git.Repo -> IO ()
|
||||||
|
genIndex g = Git.UnionMerge.stream_update_index g
|
||||||
|
[Git.UnionMerge.ls_tree fullname g]
|
||||||
|
|
||||||
|
{- Merges the specified refs into the index.
|
||||||
|
- Any changes staged in the index will be preserved. -}
|
||||||
|
mergeIndex :: [Git.Ref] -> Annex ()
|
||||||
|
mergeIndex branches = do
|
||||||
|
h <- catFileHandle
|
||||||
|
inRepo $ \g -> Git.UnionMerge.merge_index h g branches
|
||||||
|
|
||||||
|
{- Runs an action using the branch's index file. -}
|
||||||
|
withIndex :: Annex a -> Annex a
|
||||||
|
withIndex = withIndex' False
|
||||||
|
withIndex' :: Bool -> Annex a -> Annex a
|
||||||
|
withIndex' bootstrapping a = do
|
||||||
|
f <- fromRepo gitAnnexIndex
|
||||||
|
bracketIO (Git.Index.override f) id $ do
|
||||||
|
unlessM (liftIO $ doesFileExist f) $ do
|
||||||
|
unless bootstrapping create
|
||||||
|
liftIO $ createDirectoryIfMissing True $ takeDirectory f
|
||||||
|
unless bootstrapping $ inRepo genIndex
|
||||||
|
a
|
||||||
|
|
||||||
|
{- Runs an action using the branch's index file, first making sure that
|
||||||
|
- the branch and index are up-to-date. -}
|
||||||
|
withIndexUpdate :: Annex a -> Annex a
|
||||||
|
withIndexUpdate a = update >> withIndex a
|
||||||
|
|
||||||
|
{- Updates the branch's index to reflect the current contents of the branch.
|
||||||
|
- Any changes staged in the index will be preserved.
|
||||||
|
-
|
||||||
|
- Compares the ref stored in the lock file with the current
|
||||||
|
- ref of the branch to see if an update is needed.
|
||||||
|
-}
|
||||||
|
updateIndex :: Git.Ref -> Annex ()
|
||||||
|
updateIndex branchref = do
|
||||||
|
lock <- fromRepo gitAnnexIndexLock
|
||||||
|
lockref <- Git.Ref . firstLine <$>
|
||||||
|
liftIO (catchDefaultIO (readFileStrict lock) "")
|
||||||
|
when (lockref /= branchref) $ do
|
||||||
|
withIndex $ mergeIndex [fullname]
|
||||||
|
setIndexSha branchref
|
||||||
|
|
||||||
|
{- Record that the branch's index has been updated to correspond to a
|
||||||
|
- given ref of the branch. -}
|
||||||
|
setIndexSha :: Git.Ref -> Annex ()
|
||||||
|
setIndexSha ref = do
|
||||||
|
lock <- fromRepo gitAnnexIndexLock
|
||||||
|
liftIO $ writeFile lock $ show ref ++ "\n"
|
||||||
|
|
||||||
|
{- Stages the journal into the index. -}
|
||||||
|
stageJournal :: Annex ()
|
||||||
|
stageJournal = do
|
||||||
|
fs <- getJournalFiles
|
||||||
|
g <- gitRepo
|
||||||
|
withIndex $ liftIO $ do
|
||||||
|
let dir = gitAnnexJournalDir g
|
||||||
|
let paths = map (dir </>) fs
|
||||||
|
(shas, cleanup) <- Git.HashObject.hashFiles paths g
|
||||||
|
Git.UnionMerge.update_index g $
|
||||||
|
index_lines shas (map fileJournal fs)
|
||||||
|
cleanup
|
||||||
|
mapM_ removeFile paths
|
||||||
|
where
|
||||||
|
index_lines shas = map genline . zip shas
|
||||||
|
genline (sha, file) = Git.UnionMerge.update_index_line sha file
|
56
Annex/BranchState.hs
Normal file
56
Annex/BranchState.hs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{- git-annex branch state management
|
||||||
|
-
|
||||||
|
- Runtime state about the git-annex branch, including a small read cache.
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.BranchState where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Types.BranchState
|
||||||
|
import qualified Annex
|
||||||
|
|
||||||
|
getState :: Annex BranchState
|
||||||
|
getState = Annex.getState Annex.branchstate
|
||||||
|
|
||||||
|
setState :: BranchState -> Annex ()
|
||||||
|
setState state = Annex.changeState $ \s -> s { Annex.branchstate = state }
|
||||||
|
|
||||||
|
setCache :: FilePath -> String -> Annex ()
|
||||||
|
setCache file content = do
|
||||||
|
state <- getState
|
||||||
|
setState state { cachedFile = Just file, cachedContent = content }
|
||||||
|
|
||||||
|
getCache :: FilePath -> Annex (Maybe String)
|
||||||
|
getCache file = getState >>= go
|
||||||
|
where
|
||||||
|
go state
|
||||||
|
| cachedFile state == Just file =
|
||||||
|
return $ Just $ cachedContent state
|
||||||
|
| otherwise = return Nothing
|
||||||
|
|
||||||
|
invalidateCache :: Annex ()
|
||||||
|
invalidateCache = do
|
||||||
|
state <- getState
|
||||||
|
setState state { cachedFile = Nothing, cachedContent = "" }
|
||||||
|
|
||||||
|
{- Runs an action to update the branch, if it's not been updated before
|
||||||
|
- in this run of git-annex. -}
|
||||||
|
runUpdateOnce :: Annex () -> Annex ()
|
||||||
|
runUpdateOnce a = unlessM (branchUpdated <$> getState) $ do
|
||||||
|
a
|
||||||
|
disableUpdate
|
||||||
|
|
||||||
|
{- Avoids updating the branch. A useful optimisation when the branch
|
||||||
|
- is known to have not changed, or git-annex won't be relying on info
|
||||||
|
- from it. -}
|
||||||
|
disableUpdate :: Annex ()
|
||||||
|
disableUpdate = Annex.changeState setupdated
|
||||||
|
where
|
||||||
|
setupdated s = s { Annex.branchstate = new }
|
||||||
|
where
|
||||||
|
new = old { branchUpdated = True }
|
||||||
|
old = Annex.branchstate s
|
37
Annex/CatFile.hs
Normal file
37
Annex/CatFile.hs
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{- git cat-file interface, with handle automatically stored in the Annex monad
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.CatFile (
|
||||||
|
catFile,
|
||||||
|
catObject,
|
||||||
|
catFileHandle
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.CatFile
|
||||||
|
import qualified Annex
|
||||||
|
|
||||||
|
catFile :: Git.Branch -> FilePath -> Annex L.ByteString
|
||||||
|
catFile branch file = do
|
||||||
|
h <- catFileHandle
|
||||||
|
liftIO $ Git.CatFile.catFile h branch file
|
||||||
|
|
||||||
|
catObject :: Git.Ref -> Annex L.ByteString
|
||||||
|
catObject ref = do
|
||||||
|
h <- catFileHandle
|
||||||
|
liftIO $ Git.CatFile.catObject h ref
|
||||||
|
|
||||||
|
catFileHandle :: Annex Git.CatFile.CatFileHandle
|
||||||
|
catFileHandle = maybe startup return =<< Annex.getState Annex.catfilehandle
|
||||||
|
where
|
||||||
|
startup = do
|
||||||
|
h <- inRepo Git.CatFile.catFileStart
|
||||||
|
Annex.changeState $ \s -> s { Annex.catfilehandle = Just h }
|
||||||
|
return h
|
283
Annex/Content.hs
Normal file
283
Annex/Content.hs
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
{- git-annex file content managing
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.Content (
|
||||||
|
inAnnex,
|
||||||
|
inAnnexSafe,
|
||||||
|
lockContent,
|
||||||
|
calcGitLink,
|
||||||
|
logStatus,
|
||||||
|
getViaTmp,
|
||||||
|
getViaTmpUnchecked,
|
||||||
|
withTmp,
|
||||||
|
checkDiskSpace,
|
||||||
|
moveAnnex,
|
||||||
|
removeAnnex,
|
||||||
|
fromAnnex,
|
||||||
|
moveBad,
|
||||||
|
getKeysPresent,
|
||||||
|
saveState
|
||||||
|
) where
|
||||||
|
|
||||||
|
import System.IO.Error (try)
|
||||||
|
import Control.Exception (bracket_)
|
||||||
|
import System.Posix.Types
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Logs.Location
|
||||||
|
import Annex.UUID
|
||||||
|
import qualified Git
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Annex.Queue
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import Utility.StatFS
|
||||||
|
import Utility.FileMode
|
||||||
|
import Types.Key
|
||||||
|
import Utility.DataUnits
|
||||||
|
import Config
|
||||||
|
import Annex.Exception
|
||||||
|
|
||||||
|
{- Checks if a given key's content is currently present. -}
|
||||||
|
inAnnex :: Key -> Annex Bool
|
||||||
|
inAnnex = inAnnex' doesFileExist
|
||||||
|
inAnnex' :: (FilePath -> IO a) -> Key -> Annex a
|
||||||
|
inAnnex' a key = do
|
||||||
|
whenM (fromRepo Git.repoIsUrl) $
|
||||||
|
error "inAnnex cannot check remote repo"
|
||||||
|
inRepo $ \g -> gitAnnexLocation key g >>= a
|
||||||
|
|
||||||
|
{- A safer check; the key's content must not only be present, but
|
||||||
|
- is not in the process of being removed. -}
|
||||||
|
inAnnexSafe :: Key -> Annex (Maybe Bool)
|
||||||
|
inAnnexSafe = inAnnex' $ \f -> openForLock f False >>= check
|
||||||
|
where
|
||||||
|
check Nothing = return is_missing
|
||||||
|
check (Just h) = do
|
||||||
|
v <- getLock h (ReadLock, AbsoluteSeek, 0, 0)
|
||||||
|
closeFd h
|
||||||
|
return $ case v of
|
||||||
|
Just _ -> is_locked
|
||||||
|
Nothing -> is_unlocked
|
||||||
|
is_locked = Nothing
|
||||||
|
is_unlocked = Just True
|
||||||
|
is_missing = Just False
|
||||||
|
|
||||||
|
{- Content is exclusively locked while running an action that might remove
|
||||||
|
- it. (If the content is not present, no locking is done.) -}
|
||||||
|
lockContent :: Key -> Annex a -> Annex a
|
||||||
|
lockContent key a = do
|
||||||
|
file <- inRepo $ gitAnnexLocation key
|
||||||
|
bracketIO (openForLock file True >>= lock) unlock a
|
||||||
|
where
|
||||||
|
lock Nothing = return Nothing
|
||||||
|
lock (Just l) = do
|
||||||
|
v <- try $ setLock l (WriteLock, AbsoluteSeek, 0, 0)
|
||||||
|
case v of
|
||||||
|
Left _ -> error "content is locked"
|
||||||
|
Right _ -> return $ Just l
|
||||||
|
unlock Nothing = return ()
|
||||||
|
unlock (Just l) = closeFd l
|
||||||
|
|
||||||
|
openForLock :: FilePath -> Bool -> IO (Maybe Fd)
|
||||||
|
openForLock file writelock = bracket_ prep cleanup go
|
||||||
|
where
|
||||||
|
go = catchMaybeIO $ openFd file mode Nothing defaultFileFlags
|
||||||
|
mode = if writelock then ReadWrite else ReadOnly
|
||||||
|
{- Since files are stored with the write bit disabled,
|
||||||
|
- have to fiddle with permissions to open for an
|
||||||
|
- exclusive lock. -}
|
||||||
|
forwritelock a =
|
||||||
|
when writelock $ whenM (doesFileExist file) a
|
||||||
|
prep = forwritelock $ allowWrite file
|
||||||
|
cleanup = forwritelock $ preventWrite file
|
||||||
|
|
||||||
|
{- Calculates the relative path to use to link a file to a key. -}
|
||||||
|
calcGitLink :: FilePath -> Key -> Annex FilePath
|
||||||
|
calcGitLink file key = do
|
||||||
|
cwd <- liftIO getCurrentDirectory
|
||||||
|
let absfile = fromMaybe whoops $ absNormPath cwd file
|
||||||
|
loc <- inRepo $ gitAnnexLocation key
|
||||||
|
return $ relPathDirToFile (parentDir absfile) loc
|
||||||
|
where
|
||||||
|
whoops = error $ "unable to normalize " ++ file
|
||||||
|
|
||||||
|
{- Updates the Logs.Location when a key's presence changes in the current
|
||||||
|
- repository. -}
|
||||||
|
logStatus :: Key -> LogStatus -> Annex ()
|
||||||
|
logStatus key status = do
|
||||||
|
u <- getUUID
|
||||||
|
logChange key u status
|
||||||
|
|
||||||
|
{- Runs an action, passing it a temporary filename to get,
|
||||||
|
- and if the action succeeds, moves the temp file into
|
||||||
|
- the annex as a key's content. -}
|
||||||
|
getViaTmp :: Key -> (FilePath -> Annex Bool) -> Annex Bool
|
||||||
|
getViaTmp key action = do
|
||||||
|
tmp <- fromRepo $ gitAnnexTmpLocation key
|
||||||
|
|
||||||
|
-- Check that there is enough free disk space.
|
||||||
|
-- When the temp file already exists, count the space
|
||||||
|
-- it is using as free.
|
||||||
|
e <- liftIO $ doesFileExist tmp
|
||||||
|
if e
|
||||||
|
then do
|
||||||
|
stat <- liftIO $ getFileStatus tmp
|
||||||
|
checkDiskSpace' (fromIntegral $ fileSize stat) key
|
||||||
|
else checkDiskSpace key
|
||||||
|
|
||||||
|
when e $ liftIO $ allowWrite tmp
|
||||||
|
|
||||||
|
getViaTmpUnchecked key action
|
||||||
|
|
||||||
|
prepTmp :: Key -> Annex FilePath
|
||||||
|
prepTmp key = do
|
||||||
|
tmp <- fromRepo $ gitAnnexTmpLocation key
|
||||||
|
liftIO $ createDirectoryIfMissing True (parentDir tmp)
|
||||||
|
return tmp
|
||||||
|
|
||||||
|
{- Like getViaTmp, but does not check that there is enough disk space
|
||||||
|
- for the incoming key. For use when the key content is already on disk
|
||||||
|
- and not being copied into place. -}
|
||||||
|
getViaTmpUnchecked :: Key -> (FilePath -> Annex Bool) -> Annex Bool
|
||||||
|
getViaTmpUnchecked key action = do
|
||||||
|
tmp <- prepTmp key
|
||||||
|
success <- action tmp
|
||||||
|
if success
|
||||||
|
then do
|
||||||
|
moveAnnex key tmp
|
||||||
|
logStatus key InfoPresent
|
||||||
|
return True
|
||||||
|
else do
|
||||||
|
-- the tmp file is left behind, in case caller wants
|
||||||
|
-- to resume its transfer
|
||||||
|
return False
|
||||||
|
|
||||||
|
{- Creates a temp file, runs an action on it, and cleans up the temp file. -}
|
||||||
|
withTmp :: Key -> (FilePath -> Annex a) -> Annex a
|
||||||
|
withTmp key action = do
|
||||||
|
tmp <- prepTmp key
|
||||||
|
res <- action tmp
|
||||||
|
liftIO $ whenM (doesFileExist tmp) $ liftIO $ removeFile tmp
|
||||||
|
return res
|
||||||
|
|
||||||
|
{- Checks that there is disk space available to store a given key,
|
||||||
|
- throwing an error if not. -}
|
||||||
|
checkDiskSpace :: Key -> Annex ()
|
||||||
|
checkDiskSpace = checkDiskSpace' 0
|
||||||
|
|
||||||
|
checkDiskSpace' :: Integer -> Key -> Annex ()
|
||||||
|
checkDiskSpace' adjustment key = do
|
||||||
|
g <- gitRepo
|
||||||
|
r <- getConfig g "diskreserve" ""
|
||||||
|
let reserve = fromMaybe megabyte $ readSize dataUnits r
|
||||||
|
stats <- liftIO $ getFileSystemStats (gitAnnexDir g)
|
||||||
|
case (stats, keySize key) of
|
||||||
|
(Nothing, _) -> return ()
|
||||||
|
(_, Nothing) -> return ()
|
||||||
|
(Just (FileSystemStats { fsStatBytesAvailable = have }), Just need) ->
|
||||||
|
when (need + reserve > have + adjustment) $
|
||||||
|
needmorespace (need + reserve - have - adjustment)
|
||||||
|
where
|
||||||
|
megabyte :: Integer
|
||||||
|
megabyte = 1000000
|
||||||
|
needmorespace n = unlessM (Annex.getState Annex.force) $
|
||||||
|
error $ "not enough free space, need " ++
|
||||||
|
roughSize storageUnits True n ++
|
||||||
|
" more (use --force to override this check or adjust annex.diskreserve)"
|
||||||
|
|
||||||
|
{- Moves a file into .git/annex/objects/
|
||||||
|
-
|
||||||
|
- What if the key there already has content? This could happen for
|
||||||
|
- various reasons; perhaps the same content is being annexed again.
|
||||||
|
- Perhaps there has been a hash collision generating the keys.
|
||||||
|
-
|
||||||
|
- The current strategy is to assume that in this case it's safe to delete
|
||||||
|
- one of the two copies of the content; and the one already in the annex
|
||||||
|
- is left there, assuming it's the original, canonical copy.
|
||||||
|
-
|
||||||
|
- I considered being more paranoid, and checking that both files had
|
||||||
|
- the same content. Decided against it because A) users explicitly choose
|
||||||
|
- a backend based on its hashing properties and so if they're dealing
|
||||||
|
- with colliding files it's their own fault and B) adding such a check
|
||||||
|
- would not catch all cases of colliding keys. For example, perhaps
|
||||||
|
- a remote has a key; if it's then added again with different content then
|
||||||
|
- the overall system now has two different peices of content for that
|
||||||
|
- key, and one of them will probably get deleted later. So, adding the
|
||||||
|
- check here would only raise expectations that git-annex cannot truely
|
||||||
|
- meet.
|
||||||
|
-}
|
||||||
|
moveAnnex :: Key -> FilePath -> Annex ()
|
||||||
|
moveAnnex key src = do
|
||||||
|
dest <- inRepo $ gitAnnexLocation key
|
||||||
|
let dir = parentDir dest
|
||||||
|
e <- liftIO $ doesFileExist dest
|
||||||
|
if e
|
||||||
|
then liftIO $ removeFile src
|
||||||
|
else liftIO $ do
|
||||||
|
createDirectoryIfMissing True dir
|
||||||
|
allowWrite dir -- in case the directory already exists
|
||||||
|
moveFile src dest
|
||||||
|
preventWrite dest
|
||||||
|
preventWrite dir
|
||||||
|
|
||||||
|
withObjectLoc :: Key -> ((FilePath, FilePath) -> Annex a) -> Annex a
|
||||||
|
withObjectLoc key a = do
|
||||||
|
file <- inRepo $ gitAnnexLocation key
|
||||||
|
let dir = parentDir file
|
||||||
|
a (dir, file)
|
||||||
|
|
||||||
|
{- Removes a key's file from .git/annex/objects/ -}
|
||||||
|
removeAnnex :: Key -> Annex ()
|
||||||
|
removeAnnex key = withObjectLoc key $ \(dir, file) -> liftIO $ do
|
||||||
|
allowWrite dir
|
||||||
|
removeFile file
|
||||||
|
removeDirectory dir
|
||||||
|
|
||||||
|
{- Moves a key's file out of .git/annex/objects/ -}
|
||||||
|
fromAnnex :: Key -> FilePath -> Annex ()
|
||||||
|
fromAnnex key dest = withObjectLoc key $ \(dir, file) -> liftIO $ do
|
||||||
|
allowWrite dir
|
||||||
|
allowWrite file
|
||||||
|
moveFile file dest
|
||||||
|
removeDirectory dir
|
||||||
|
|
||||||
|
{- Moves a key out of .git/annex/objects/ into .git/annex/bad, and
|
||||||
|
- returns the file it was moved to. -}
|
||||||
|
moveBad :: Key -> Annex FilePath
|
||||||
|
moveBad key = do
|
||||||
|
src <- inRepo $ gitAnnexLocation key
|
||||||
|
bad <- fromRepo gitAnnexBadDir
|
||||||
|
let dest = bad </> takeFileName src
|
||||||
|
liftIO $ do
|
||||||
|
createDirectoryIfMissing True (parentDir dest)
|
||||||
|
allowWrite (parentDir src)
|
||||||
|
moveFile src dest
|
||||||
|
removeDirectory (parentDir src)
|
||||||
|
logStatus key InfoMissing
|
||||||
|
return dest
|
||||||
|
|
||||||
|
{- List of keys whose content exists in .git/annex/objects/ -}
|
||||||
|
getKeysPresent :: Annex [Key]
|
||||||
|
getKeysPresent = getKeysPresent' =<< fromRepo gitAnnexObjectDir
|
||||||
|
getKeysPresent' :: FilePath -> Annex [Key]
|
||||||
|
getKeysPresent' dir = do
|
||||||
|
exists <- liftIO $ doesDirectoryExist dir
|
||||||
|
if not exists
|
||||||
|
then return []
|
||||||
|
else liftIO $ do
|
||||||
|
-- 2 levels of hashing
|
||||||
|
levela <- dirContents dir
|
||||||
|
levelb <- mapM dirContents levela
|
||||||
|
contents <- mapM dirContents (concat levelb)
|
||||||
|
let files = concat contents
|
||||||
|
return $ mapMaybe (fileKey . takeFileName) files
|
||||||
|
|
||||||
|
{- Things to do to record changes to content. -}
|
||||||
|
saveState :: Annex ()
|
||||||
|
saveState = do
|
||||||
|
Annex.Queue.flush False
|
||||||
|
Annex.Branch.commit "update"
|
27
Annex/Exception.hs
Normal file
27
Annex/Exception.hs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{- exception handling in the git-annex monad
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.Exception (
|
||||||
|
bracketIO,
|
||||||
|
handle,
|
||||||
|
throw,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Exception.Control (handle)
|
||||||
|
import Control.Monad.IO.Control (liftIOOp)
|
||||||
|
import Control.Exception hiding (handle, throw)
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
|
||||||
|
{- Runs an Annex action, with setup and cleanup both in the IO monad. -}
|
||||||
|
bracketIO :: IO c -> (c -> IO b) -> Annex a -> Annex a
|
||||||
|
bracketIO setup cleanup go =
|
||||||
|
liftIOOp (Control.Exception.bracket setup cleanup) (const go)
|
||||||
|
|
||||||
|
{- Throws an exception in the Annex monad. -}
|
||||||
|
throw :: Control.Exception.Exception e => e -> Annex a
|
||||||
|
throw = liftIO . throwIO
|
94
Annex/Journal.hs
Normal file
94
Annex/Journal.hs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
{- management of the git-annex journal and cache
|
||||||
|
-
|
||||||
|
- The journal is used to queue up changes before they are committed to the
|
||||||
|
- git-annex branch. Amoung other things, it ensures that if git-annex is
|
||||||
|
- interrupted, its recorded data is not lost.
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.Journal where
|
||||||
|
|
||||||
|
import System.IO.Binary
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Annex.Exception
|
||||||
|
import qualified Git
|
||||||
|
|
||||||
|
{- Records content for a file in the branch to the journal.
|
||||||
|
-
|
||||||
|
- Using the journal, rather than immediatly staging content to the index
|
||||||
|
- avoids git needing to rewrite the index after every change. -}
|
||||||
|
setJournalFile :: FilePath -> String -> Annex ()
|
||||||
|
setJournalFile file content = do
|
||||||
|
g <- gitRepo
|
||||||
|
liftIO $ doRedo (write g) $ do
|
||||||
|
createDirectoryIfMissing True $ gitAnnexJournalDir g
|
||||||
|
createDirectoryIfMissing True $ gitAnnexTmpDir g
|
||||||
|
where
|
||||||
|
-- journal file is written atomically
|
||||||
|
write g = do
|
||||||
|
let jfile = journalFile g file
|
||||||
|
let tmpfile = gitAnnexTmpDir g </> takeFileName jfile
|
||||||
|
writeBinaryFile tmpfile content
|
||||||
|
moveFile tmpfile jfile
|
||||||
|
|
||||||
|
{- Gets any journalled content for a file in the branch. -}
|
||||||
|
getJournalFile :: FilePath -> Annex (Maybe String)
|
||||||
|
getJournalFile file = inRepo $ \g -> catchMaybeIO $
|
||||||
|
readFileStrict $ journalFile g file
|
||||||
|
|
||||||
|
{- List of files that have updated content in the journal. -}
|
||||||
|
getJournalledFiles :: Annex [FilePath]
|
||||||
|
getJournalledFiles = map fileJournal <$> getJournalFiles
|
||||||
|
|
||||||
|
{- List of existing journal files. -}
|
||||||
|
getJournalFiles :: Annex [FilePath]
|
||||||
|
getJournalFiles = do
|
||||||
|
g <- gitRepo
|
||||||
|
fs <- liftIO $
|
||||||
|
catchDefaultIO (getDirectoryContents $ gitAnnexJournalDir g) []
|
||||||
|
return $ filter (`notElem` [".", ".."]) fs
|
||||||
|
|
||||||
|
{- Checks if there are changes in the journal. -}
|
||||||
|
journalDirty :: Annex Bool
|
||||||
|
journalDirty = not . null <$> getJournalFiles
|
||||||
|
|
||||||
|
{- Produces a filename to use in the journal for a file on the branch.
|
||||||
|
-
|
||||||
|
- The journal typically won't have a lot of files in it, so the hashing
|
||||||
|
- used in the branch is not necessary, and all the files are put directly
|
||||||
|
- in the journal directory.
|
||||||
|
-}
|
||||||
|
journalFile :: Git.Repo -> FilePath -> FilePath
|
||||||
|
journalFile repo file = gitAnnexJournalDir repo </> concatMap mangle file
|
||||||
|
where
|
||||||
|
mangle '/' = "_"
|
||||||
|
mangle '_' = "__"
|
||||||
|
mangle c = [c]
|
||||||
|
|
||||||
|
{- Converts a journal file (relative to the journal dir) back to the
|
||||||
|
- filename on the branch. -}
|
||||||
|
fileJournal :: FilePath -> FilePath
|
||||||
|
fileJournal = replace "//" "_" . replace "_" "/"
|
||||||
|
|
||||||
|
{- Runs an action that modifies the journal, using locking to avoid
|
||||||
|
- contention with other git-annex processes. -}
|
||||||
|
lockJournal :: Annex a -> Annex a
|
||||||
|
lockJournal a = do
|
||||||
|
file <- fromRepo gitAnnexJournalLock
|
||||||
|
bracketIO (lock file) unlock a
|
||||||
|
where
|
||||||
|
lock file = do
|
||||||
|
l <- doRedo (createFile file stdFileMode) $
|
||||||
|
createDirectoryIfMissing True $ takeDirectory file
|
||||||
|
waitToSetLock l (WriteLock, AbsoluteSeek, 0, 0)
|
||||||
|
return l
|
||||||
|
unlock = closeFd
|
||||||
|
|
||||||
|
{- Runs an action, catching failure and running something to fix it up, and
|
||||||
|
- retrying if necessary. -}
|
||||||
|
doRedo :: IO a -> IO b -> IO a
|
||||||
|
doRedo a b = catch a $ const $ b >> a
|
41
Annex/Queue.hs
Normal file
41
Annex/Queue.hs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{- git-annex command queue
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.Queue (
|
||||||
|
add,
|
||||||
|
flush,
|
||||||
|
flushWhenFull
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Annex
|
||||||
|
import qualified Git.Queue
|
||||||
|
|
||||||
|
{- Adds a git command to the queue. -}
|
||||||
|
add :: String -> [CommandParam] -> [FilePath] -> Annex ()
|
||||||
|
add command params files = do
|
||||||
|
q <- getState repoqueue
|
||||||
|
store $ Git.Queue.add q command params files
|
||||||
|
|
||||||
|
{- Runs the queue if it is full. Should be called periodically. -}
|
||||||
|
flushWhenFull :: Annex ()
|
||||||
|
flushWhenFull = do
|
||||||
|
q <- getState repoqueue
|
||||||
|
when (Git.Queue.full q) $ flush False
|
||||||
|
|
||||||
|
{- Runs (and empties) the queue. -}
|
||||||
|
flush :: Bool -> Annex ()
|
||||||
|
flush silent = do
|
||||||
|
q <- getState repoqueue
|
||||||
|
unless (0 == Git.Queue.size q) $ do
|
||||||
|
unless silent $
|
||||||
|
showSideAction "Recording state in git"
|
||||||
|
q' <- inRepo $ Git.Queue.flush q
|
||||||
|
store q'
|
||||||
|
|
||||||
|
store :: Git.Queue.Queue -> Annex ()
|
||||||
|
store q = changeState $ \s -> s { repoqueue = q }
|
65
Annex/Ssh.hs
Normal file
65
Annex/Ssh.hs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{- git-annex remote access with ssh
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.Ssh where
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Url
|
||||||
|
import Types
|
||||||
|
import Config
|
||||||
|
import Annex.UUID
|
||||||
|
|
||||||
|
{- Generates parameters to ssh to a repository's host and run a command.
|
||||||
|
- Caller is responsible for doing any neccessary shellEscaping of the
|
||||||
|
- passed command. -}
|
||||||
|
sshToRepo :: Git.Repo -> [CommandParam] -> Annex [CommandParam]
|
||||||
|
sshToRepo repo sshcmd = do
|
||||||
|
s <- getConfig repo "ssh-options" ""
|
||||||
|
let sshoptions = map Param (words s)
|
||||||
|
let sshport = case Git.Url.port repo of
|
||||||
|
Nothing -> []
|
||||||
|
Just p -> [Param "-p", Param (show p)]
|
||||||
|
let sshhost = Param $ Git.Url.hostuser repo
|
||||||
|
return $ sshoptions ++ sshport ++ [sshhost] ++ sshcmd
|
||||||
|
|
||||||
|
{- Generates parameters to run a git-annex-shell command on a remote
|
||||||
|
- repository. -}
|
||||||
|
git_annex_shell :: Git.Repo -> String -> [CommandParam] -> Annex (Maybe (FilePath, [CommandParam]))
|
||||||
|
git_annex_shell r command params
|
||||||
|
| not $ Git.repoIsUrl r = return $ Just (shellcmd, shellopts)
|
||||||
|
| Git.repoIsSsh r = do
|
||||||
|
uuid <- getRepoUUID r
|
||||||
|
sshparams <- sshToRepo r [Param $ sshcmd uuid ]
|
||||||
|
return $ Just ("ssh", sshparams)
|
||||||
|
| otherwise = return Nothing
|
||||||
|
where
|
||||||
|
dir = Git.workTree r
|
||||||
|
shellcmd = "git-annex-shell"
|
||||||
|
shellopts = Param command : File dir : params
|
||||||
|
sshcmd uuid = unwords $
|
||||||
|
shellcmd : map shellEscape (toCommand shellopts) ++
|
||||||
|
uuidcheck uuid
|
||||||
|
uuidcheck NoUUID = []
|
||||||
|
uuidcheck (UUID u) = ["--uuid", u]
|
||||||
|
|
||||||
|
{- Uses a supplied function (such as boolSystem) to run a git-annex-shell
|
||||||
|
- command on a remote.
|
||||||
|
-
|
||||||
|
- Or, if the remote does not support running remote commands, returns
|
||||||
|
- a specified error value. -}
|
||||||
|
onRemote
|
||||||
|
:: Git.Repo
|
||||||
|
-> (FilePath -> [CommandParam] -> IO a, a)
|
||||||
|
-> String
|
||||||
|
-> [CommandParam]
|
||||||
|
-> Annex a
|
||||||
|
onRemote r (with, errorval) command params = do
|
||||||
|
s <- git_annex_shell r command params
|
||||||
|
case s of
|
||||||
|
Just (c, ps) -> liftIO $ with c ps
|
||||||
|
Nothing -> return errorval
|
74
Annex/UUID.hs
Normal file
74
Annex/UUID.hs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{- git-annex uuids
|
||||||
|
-
|
||||||
|
- Each git repository used by git-annex has an annex.uuid setting that
|
||||||
|
- uniquely identifies that repository.
|
||||||
|
-
|
||||||
|
- UUIDs of remotes are cached in git config, using keys named
|
||||||
|
- remote.<name>.annex-uuid
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.UUID (
|
||||||
|
getUUID,
|
||||||
|
getRepoUUID,
|
||||||
|
getUncachedUUID,
|
||||||
|
prepUUID,
|
||||||
|
genUUID
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Config
|
||||||
|
import qualified Build.SysConfig as SysConfig
|
||||||
|
import Config
|
||||||
|
|
||||||
|
configkey :: String
|
||||||
|
configkey = "annex.uuid"
|
||||||
|
|
||||||
|
{- Generates a UUID. There is a library for this, but it's not packaged,
|
||||||
|
- so use the command line tool. -}
|
||||||
|
genUUID :: IO UUID
|
||||||
|
genUUID = pOpen ReadFromPipe command params $ liftM toUUID . hGetLine
|
||||||
|
where
|
||||||
|
command = SysConfig.uuid
|
||||||
|
params = if command == "uuid"
|
||||||
|
-- request a random uuid be generated
|
||||||
|
then ["-m"]
|
||||||
|
-- uuidgen generates random uuid by default
|
||||||
|
else []
|
||||||
|
|
||||||
|
{- Get current repository's UUID. -}
|
||||||
|
getUUID :: Annex UUID
|
||||||
|
getUUID = getRepoUUID =<< gitRepo
|
||||||
|
|
||||||
|
{- Looks up a repo's UUID, caching it in .git/config if it's not already. -}
|
||||||
|
getRepoUUID :: Git.Repo -> Annex UUID
|
||||||
|
getRepoUUID r = do
|
||||||
|
c <- fromRepo cached
|
||||||
|
let u = getUncachedUUID r
|
||||||
|
|
||||||
|
if c /= u && u /= NoUUID
|
||||||
|
then do
|
||||||
|
updatecache u
|
||||||
|
return u
|
||||||
|
else return c
|
||||||
|
where
|
||||||
|
cached = toUUID . Git.Config.get cachekey ""
|
||||||
|
updatecache u = do
|
||||||
|
g <- gitRepo
|
||||||
|
when (g /= r) $ storeUUID cachekey u
|
||||||
|
cachekey = remoteConfig r "uuid"
|
||||||
|
|
||||||
|
getUncachedUUID :: Git.Repo -> UUID
|
||||||
|
getUncachedUUID = toUUID . Git.Config.get configkey ""
|
||||||
|
|
||||||
|
{- Make sure that the repo has an annex.uuid setting. -}
|
||||||
|
prepUUID :: Annex ()
|
||||||
|
prepUUID = whenM ((==) NoUUID <$> getUUID) $
|
||||||
|
storeUUID configkey =<< liftIO genUUID
|
||||||
|
|
||||||
|
storeUUID :: String -> UUID -> Annex ()
|
||||||
|
storeUUID configfield = setConfig configfield . fromUUID
|
44
Annex/Version.hs
Normal file
44
Annex/Version.hs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{- git-annex repository versioning
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Annex.Version where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Git.Config
|
||||||
|
import Config
|
||||||
|
|
||||||
|
type Version = String
|
||||||
|
|
||||||
|
defaultVersion :: Version
|
||||||
|
defaultVersion = "3"
|
||||||
|
|
||||||
|
supportedVersions :: [Version]
|
||||||
|
supportedVersions = [defaultVersion]
|
||||||
|
|
||||||
|
upgradableVersions :: [Version]
|
||||||
|
upgradableVersions = ["0", "1", "2"]
|
||||||
|
|
||||||
|
versionField :: String
|
||||||
|
versionField = "annex.version"
|
||||||
|
|
||||||
|
getVersion :: Annex (Maybe Version)
|
||||||
|
getVersion = handle <$> fromRepo (Git.Config.get versionField "")
|
||||||
|
where
|
||||||
|
handle [] = Nothing
|
||||||
|
handle v = Just v
|
||||||
|
|
||||||
|
setVersion :: Annex ()
|
||||||
|
setVersion = setConfig versionField defaultVersion
|
||||||
|
|
||||||
|
checkVersion :: Version -> Annex ()
|
||||||
|
checkVersion v
|
||||||
|
| v `elem` supportedVersions = return ()
|
||||||
|
| v `elem` upgradableVersions = err "Upgrade this repository: git-annex upgrade"
|
||||||
|
| otherwise = err "Upgrade git-annex."
|
||||||
|
where
|
||||||
|
err msg = error $ "Repository version " ++ v ++
|
||||||
|
" is not supported. " ++ msg
|
120
Backend.hs
Normal file
120
Backend.hs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
{- git-annex key/value backends
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Backend (
|
||||||
|
BackendFile,
|
||||||
|
list,
|
||||||
|
orderedList,
|
||||||
|
genKey,
|
||||||
|
lookupFile,
|
||||||
|
chooseBackends,
|
||||||
|
lookupBackendName,
|
||||||
|
maybeLookupBackendName
|
||||||
|
) where
|
||||||
|
|
||||||
|
import System.IO.Error (try)
|
||||||
|
import System.Posix.Files
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Git.Config
|
||||||
|
import qualified Git.CheckAttr
|
||||||
|
import qualified Annex
|
||||||
|
import Types.Key
|
||||||
|
import qualified Types.Backend as B
|
||||||
|
|
||||||
|
-- When adding a new backend, import it here and add it to the list.
|
||||||
|
import qualified Backend.SHA
|
||||||
|
import qualified Backend.WORM
|
||||||
|
import qualified Backend.URL
|
||||||
|
|
||||||
|
list :: [Backend Annex]
|
||||||
|
list = Backend.SHA.backends ++ Backend.WORM.backends ++ Backend.URL.backends
|
||||||
|
|
||||||
|
{- List of backends in the order to try them when storing a new key. -}
|
||||||
|
orderedList :: Annex [Backend Annex]
|
||||||
|
orderedList = do
|
||||||
|
l <- Annex.getState Annex.backends -- list is cached here
|
||||||
|
if not $ null l
|
||||||
|
then return l
|
||||||
|
else handle =<< Annex.getState Annex.forcebackend
|
||||||
|
where
|
||||||
|
handle Nothing = standard
|
||||||
|
handle (Just "") = standard
|
||||||
|
handle (Just name) = do
|
||||||
|
l' <- (lookupBackendName name :) <$> standard
|
||||||
|
Annex.changeState $ \s -> s { Annex.backends = l' }
|
||||||
|
return l'
|
||||||
|
standard = fromRepo $ parseBackendList . Git.Config.get "annex.backends" ""
|
||||||
|
parseBackendList [] = list
|
||||||
|
parseBackendList s = map lookupBackendName $ words s
|
||||||
|
|
||||||
|
{- Generates a key for a file, trying each backend in turn until one
|
||||||
|
- accepts it. -}
|
||||||
|
genKey :: FilePath -> Maybe (Backend Annex) -> Annex (Maybe (Key, Backend Annex))
|
||||||
|
genKey file trybackend = do
|
||||||
|
bs <- orderedList
|
||||||
|
let bs' = maybe bs (: bs) trybackend
|
||||||
|
genKey' bs' file
|
||||||
|
genKey' :: [Backend Annex] -> FilePath -> Annex (Maybe (Key, Backend Annex))
|
||||||
|
genKey' [] _ = return Nothing
|
||||||
|
genKey' (b:bs) file = do
|
||||||
|
r <- (B.getKey b) file
|
||||||
|
case r of
|
||||||
|
Nothing -> genKey' bs file
|
||||||
|
Just k -> return $ Just (makesane k, b)
|
||||||
|
where
|
||||||
|
-- keyNames should not contain newline characters.
|
||||||
|
makesane k = k { keyName = map fixbadchar (keyName k) }
|
||||||
|
fixbadchar c
|
||||||
|
| c == '\n' = '_'
|
||||||
|
| otherwise = c
|
||||||
|
|
||||||
|
{- Looks up the key and backend corresponding to an annexed file,
|
||||||
|
- by examining what the file symlinks to. -}
|
||||||
|
lookupFile :: FilePath -> Annex (Maybe (Key, Backend Annex))
|
||||||
|
lookupFile file = do
|
||||||
|
tl <- liftIO $ try getsymlink
|
||||||
|
case tl of
|
||||||
|
Left _ -> return Nothing
|
||||||
|
Right l -> makekey l
|
||||||
|
where
|
||||||
|
getsymlink = takeFileName <$> readSymbolicLink file
|
||||||
|
makekey l = maybe (return Nothing) (makeret l) (fileKey l)
|
||||||
|
makeret l k = let bname = keyBackendName k in
|
||||||
|
case maybeLookupBackendName bname of
|
||||||
|
Just backend -> return $ Just (k, backend)
|
||||||
|
Nothing -> do
|
||||||
|
when (isLinkToAnnex l) $ warning $
|
||||||
|
"skipping " ++ file ++
|
||||||
|
" (unknown backend " ++
|
||||||
|
bname ++ ")"
|
||||||
|
return Nothing
|
||||||
|
|
||||||
|
type BackendFile = (Maybe (Backend Annex), FilePath)
|
||||||
|
|
||||||
|
{- Looks up the backends that should be used for each file in a list.
|
||||||
|
- That can be configured on a per-file basis in the gitattributes file.
|
||||||
|
-}
|
||||||
|
chooseBackends :: [FilePath] -> Annex [BackendFile]
|
||||||
|
chooseBackends fs = Annex.getState Annex.forcebackend >>= go
|
||||||
|
where
|
||||||
|
go Nothing = do
|
||||||
|
pairs <- inRepo $ Git.CheckAttr.lookup "annex.backend" fs
|
||||||
|
return $ map (\(f,b) -> (maybeLookupBackendName b, f)) pairs
|
||||||
|
go (Just _) = do
|
||||||
|
l <- orderedList
|
||||||
|
return $ map (\f -> (Just $ Prelude.head l, f)) fs
|
||||||
|
|
||||||
|
{- Looks up a backend by name. May fail if unknown. -}
|
||||||
|
lookupBackendName :: String -> Backend Annex
|
||||||
|
lookupBackendName s = fromMaybe unknown $ maybeLookupBackendName s
|
||||||
|
where
|
||||||
|
unknown = error $ "unknown backend " ++ s
|
||||||
|
maybeLookupBackendName :: String -> Maybe (Backend Annex)
|
||||||
|
maybeLookupBackendName s = headMaybe matches
|
||||||
|
where
|
||||||
|
matches = filter (\b -> s == B.name b) list
|
114
Backend/SHA.hs
Normal file
114
Backend/SHA.hs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
{- git-annex SHA backend
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Backend.SHA (backends) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex
|
||||||
|
import Annex.Content
|
||||||
|
import Types.Backend
|
||||||
|
import Types.Key
|
||||||
|
import qualified Build.SysConfig as SysConfig
|
||||||
|
|
||||||
|
type SHASize = Int
|
||||||
|
|
||||||
|
-- order is slightly significant; want SHA256 first, and more general
|
||||||
|
-- sizes earlier
|
||||||
|
sizes :: [Int]
|
||||||
|
sizes = [256, 1, 512, 224, 384]
|
||||||
|
|
||||||
|
backends :: [Backend Annex]
|
||||||
|
backends = catMaybes $ map genBackend sizes ++ map genBackendE sizes
|
||||||
|
|
||||||
|
genBackend :: SHASize -> Maybe (Backend Annex)
|
||||||
|
genBackend size
|
||||||
|
| isNothing (shaCommand size) = Nothing
|
||||||
|
| otherwise = Just b
|
||||||
|
where
|
||||||
|
b = Types.Backend.Backend
|
||||||
|
{ name = shaName size
|
||||||
|
, getKey = keyValue size
|
||||||
|
, fsckKey = checkKeyChecksum size
|
||||||
|
}
|
||||||
|
|
||||||
|
genBackendE :: SHASize -> Maybe (Backend Annex)
|
||||||
|
genBackendE size =
|
||||||
|
case genBackend size of
|
||||||
|
Nothing -> Nothing
|
||||||
|
Just b -> Just $ b
|
||||||
|
{ name = shaNameE size
|
||||||
|
, getKey = keyValueE size
|
||||||
|
}
|
||||||
|
|
||||||
|
shaCommand :: SHASize -> Maybe String
|
||||||
|
shaCommand 1 = SysConfig.sha1
|
||||||
|
shaCommand 256 = SysConfig.sha256
|
||||||
|
shaCommand 224 = SysConfig.sha224
|
||||||
|
shaCommand 384 = SysConfig.sha384
|
||||||
|
shaCommand 512 = SysConfig.sha512
|
||||||
|
shaCommand _ = Nothing
|
||||||
|
|
||||||
|
shaName :: SHASize -> String
|
||||||
|
shaName size = "SHA" ++ show size
|
||||||
|
|
||||||
|
shaNameE :: SHASize -> String
|
||||||
|
shaNameE size = shaName size ++ "E"
|
||||||
|
|
||||||
|
shaN :: SHASize -> FilePath -> Annex String
|
||||||
|
shaN size file = do
|
||||||
|
showAction "checksum"
|
||||||
|
liftIO $ pOpen ReadFromPipe command (toCommand [File file]) $ \h -> do
|
||||||
|
sha <- fst . separate (== ' ') <$> hGetLine h
|
||||||
|
if null sha
|
||||||
|
then error $ command ++ " parse error"
|
||||||
|
else return sha
|
||||||
|
where
|
||||||
|
command = fromJust $ shaCommand size
|
||||||
|
|
||||||
|
{- A key is a checksum of its contents. -}
|
||||||
|
keyValue :: SHASize -> FilePath -> Annex (Maybe Key)
|
||||||
|
keyValue size file = do
|
||||||
|
s <- shaN size file
|
||||||
|
stat <- liftIO $ getFileStatus file
|
||||||
|
return $ Just $ stubKey
|
||||||
|
{ keyName = s
|
||||||
|
, keyBackendName = shaName size
|
||||||
|
, keySize = Just $ fromIntegral $ fileSize stat
|
||||||
|
}
|
||||||
|
|
||||||
|
{- Extension preserving keys. -}
|
||||||
|
keyValueE :: SHASize -> FilePath -> Annex (Maybe Key)
|
||||||
|
keyValueE size file = keyValue size file >>= maybe (return Nothing) addE
|
||||||
|
where
|
||||||
|
addE k = return $ Just $ k
|
||||||
|
{ keyName = keyName k ++ extension
|
||||||
|
, keyBackendName = shaNameE size
|
||||||
|
}
|
||||||
|
naiveextension = takeExtension file
|
||||||
|
extension
|
||||||
|
-- long or newline containing extensions are
|
||||||
|
-- probably not really an extension
|
||||||
|
| length naiveextension > 6 ||
|
||||||
|
'\n' `elem` naiveextension = ""
|
||||||
|
| otherwise = naiveextension
|
||||||
|
|
||||||
|
{- A key's checksum is checked during fsck. -}
|
||||||
|
checkKeyChecksum :: SHASize -> Key -> Annex Bool
|
||||||
|
checkKeyChecksum size key = do
|
||||||
|
fast <- Annex.getState Annex.fast
|
||||||
|
file <- inRepo $ gitAnnexLocation key
|
||||||
|
present <- liftIO $ doesFileExist file
|
||||||
|
if not present || fast
|
||||||
|
then return True
|
||||||
|
else check =<< shaN size file
|
||||||
|
where
|
||||||
|
check s
|
||||||
|
| s == dropExtension (keyName key) = return True
|
||||||
|
| otherwise = do
|
||||||
|
dest <- moveBad key
|
||||||
|
warning $ "Bad file content; moved to " ++ dest
|
||||||
|
return False
|
28
Backend/URL.hs
Normal file
28
Backend/URL.hs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{- git-annex "URL" backend -- keys whose content is available from urls.
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Backend.URL (
|
||||||
|
backends,
|
||||||
|
fromUrl
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Types.Backend
|
||||||
|
import Types.Key
|
||||||
|
|
||||||
|
backends :: [Backend Annex]
|
||||||
|
backends = [backend]
|
||||||
|
|
||||||
|
backend :: Backend Annex
|
||||||
|
backend = Types.Backend.Backend {
|
||||||
|
name = "URL",
|
||||||
|
getKey = const (return Nothing),
|
||||||
|
fsckKey = const (return True)
|
||||||
|
}
|
||||||
|
|
||||||
|
fromUrl :: String -> Key
|
||||||
|
fromUrl url = stubKey { keyName = url, keyBackendName = "URL" }
|
39
Backend/WORM.hs
Normal file
39
Backend/WORM.hs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{- git-annex "WORM" backend -- Write Once, Read Many
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Backend.WORM (backends) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Types.Backend
|
||||||
|
import Types.Key
|
||||||
|
|
||||||
|
backends :: [Backend Annex]
|
||||||
|
backends = [backend]
|
||||||
|
|
||||||
|
backend :: Backend Annex
|
||||||
|
backend = Types.Backend.Backend {
|
||||||
|
name = "WORM",
|
||||||
|
getKey = keyValue,
|
||||||
|
fsckKey = const (return True)
|
||||||
|
}
|
||||||
|
|
||||||
|
{- The key includes the file size, modification time, and the
|
||||||
|
- basename of the filename.
|
||||||
|
-
|
||||||
|
- That allows multiple files with the same names to have different keys,
|
||||||
|
- while also allowing a file to be moved around while retaining the
|
||||||
|
- same key.
|
||||||
|
-}
|
||||||
|
keyValue :: FilePath -> Annex (Maybe Key)
|
||||||
|
keyValue file = do
|
||||||
|
stat <- liftIO $ getFileStatus file
|
||||||
|
return $ Just Key {
|
||||||
|
keyName = takeFileName file,
|
||||||
|
keyBackendName = name backend,
|
||||||
|
keySize = Just $ fromIntegral $ fileSize stat,
|
||||||
|
keyMtime = Just $ modificationTime stat
|
||||||
|
}
|
114
Build/TestConfig.hs
Normal file
114
Build/TestConfig.hs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
{- Tests the system and generates Build.SysConfig.hs. -}
|
||||||
|
|
||||||
|
module Build.TestConfig where
|
||||||
|
|
||||||
|
import System.IO
|
||||||
|
import System.Cmd
|
||||||
|
import System.Exit
|
||||||
|
|
||||||
|
type ConfigKey = String
|
||||||
|
data ConfigValue =
|
||||||
|
BoolConfig Bool |
|
||||||
|
StringConfig String |
|
||||||
|
MaybeStringConfig (Maybe String)
|
||||||
|
data Config = Config ConfigKey ConfigValue
|
||||||
|
|
||||||
|
type Test = IO Config
|
||||||
|
type TestName = String
|
||||||
|
data TestCase = TestCase TestName Test
|
||||||
|
|
||||||
|
instance Show ConfigValue where
|
||||||
|
show (BoolConfig b) = show b
|
||||||
|
show (StringConfig s) = show s
|
||||||
|
show (MaybeStringConfig s) = show s
|
||||||
|
|
||||||
|
instance Show Config where
|
||||||
|
show (Config key value) = unlines
|
||||||
|
[ key ++ " :: " ++ valuetype value
|
||||||
|
, key ++ " = " ++ show value
|
||||||
|
]
|
||||||
|
where
|
||||||
|
valuetype (BoolConfig _) = "Bool"
|
||||||
|
valuetype (StringConfig _) = "String"
|
||||||
|
valuetype (MaybeStringConfig _) = "Maybe String"
|
||||||
|
|
||||||
|
writeSysConfig :: [Config] -> IO ()
|
||||||
|
writeSysConfig config = writeFile "Build/SysConfig.hs" body
|
||||||
|
where
|
||||||
|
body = unlines $ header ++ map show config ++ footer
|
||||||
|
header = [
|
||||||
|
"{- Automatically generated. -}"
|
||||||
|
, "module Build.SysConfig where"
|
||||||
|
, ""
|
||||||
|
]
|
||||||
|
footer = []
|
||||||
|
|
||||||
|
runTests :: [TestCase] -> IO [Config]
|
||||||
|
runTests [] = return []
|
||||||
|
runTests (TestCase tname t : ts) = do
|
||||||
|
testStart tname
|
||||||
|
c <- t
|
||||||
|
testEnd c
|
||||||
|
rest <- runTests ts
|
||||||
|
return $ c:rest
|
||||||
|
|
||||||
|
{- Tests that a command is available, aborting if not. -}
|
||||||
|
requireCmd :: ConfigKey -> String -> Test
|
||||||
|
requireCmd k cmdline = do
|
||||||
|
ret <- testCmd k cmdline
|
||||||
|
handle ret
|
||||||
|
where
|
||||||
|
handle r@(Config _ (BoolConfig True)) = return r
|
||||||
|
handle r = do
|
||||||
|
testEnd r
|
||||||
|
error $ "** the " ++ c ++ " command is required"
|
||||||
|
c = head $ words cmdline
|
||||||
|
|
||||||
|
{- Checks if a command is available by running a command line. -}
|
||||||
|
testCmd :: ConfigKey -> String -> Test
|
||||||
|
testCmd k cmdline = do
|
||||||
|
ret <- system $ quiet cmdline
|
||||||
|
return $ Config k (BoolConfig $ ret == ExitSuccess)
|
||||||
|
|
||||||
|
{- Ensures that one of a set of commands is available by running each in
|
||||||
|
- turn. The Config is set to the first one found. -}
|
||||||
|
selectCmd :: ConfigKey -> [String] -> String -> Test
|
||||||
|
selectCmd k = searchCmd
|
||||||
|
(return . Config k . StringConfig)
|
||||||
|
(\cmds -> do
|
||||||
|
testEnd $ Config k $ BoolConfig False
|
||||||
|
error $ "* need one of these commands, but none are available: " ++ show cmds
|
||||||
|
)
|
||||||
|
|
||||||
|
maybeSelectCmd :: ConfigKey -> [String] -> String -> Test
|
||||||
|
maybeSelectCmd k = searchCmd
|
||||||
|
(return . Config k . MaybeStringConfig . Just)
|
||||||
|
(\_ -> return $ Config k $ MaybeStringConfig Nothing)
|
||||||
|
|
||||||
|
searchCmd :: (String -> Test) -> ([String] -> Test) -> [String] -> String -> Test
|
||||||
|
searchCmd success failure cmds param = search cmds
|
||||||
|
where
|
||||||
|
search [] = failure cmds
|
||||||
|
search (c:cs) = do
|
||||||
|
ret <- system $ quiet c ++ " " ++ param
|
||||||
|
if ret == ExitSuccess
|
||||||
|
then success c
|
||||||
|
else search cs
|
||||||
|
|
||||||
|
quiet :: String -> String
|
||||||
|
quiet s = s ++ " >/dev/null 2>&1"
|
||||||
|
|
||||||
|
testStart :: TestName -> IO ()
|
||||||
|
testStart s = do
|
||||||
|
putStr $ " checking " ++ s ++ "..."
|
||||||
|
hFlush stdout
|
||||||
|
|
||||||
|
testEnd :: Config -> IO ()
|
||||||
|
testEnd (Config _ (BoolConfig True)) = status "yes"
|
||||||
|
testEnd (Config _ (BoolConfig False)) = status "no"
|
||||||
|
testEnd (Config _ (StringConfig s)) = status s
|
||||||
|
testEnd (Config _ (MaybeStringConfig (Just s))) = status s
|
||||||
|
testEnd (Config _ (MaybeStringConfig Nothing)) = status "not available"
|
||||||
|
|
||||||
|
status :: String -> IO ()
|
||||||
|
status s = putStrLn $ ' ':s
|
1
CHANGELOG
Symbolic link
1
CHANGELOG
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
debian/changelog
|
42
Checks.hs
Normal file
42
Checks.hs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
{- git-annex command checks
|
||||||
|
-
|
||||||
|
- Common sanity checks for commands, and an interface to selectively
|
||||||
|
- remove them, or add others.
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Checks where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Types.Command
|
||||||
|
import Init
|
||||||
|
import qualified Annex
|
||||||
|
|
||||||
|
commonChecks :: [CommandCheck]
|
||||||
|
commonChecks = [fromOpt, toOpt, repoExists]
|
||||||
|
|
||||||
|
repoExists :: CommandCheck
|
||||||
|
repoExists = CommandCheck 0 ensureInitialized
|
||||||
|
|
||||||
|
fromOpt :: CommandCheck
|
||||||
|
fromOpt = CommandCheck 1 $ do
|
||||||
|
v <- Annex.getState Annex.fromremote
|
||||||
|
unless (isNothing v) $ error "cannot use --from with this command"
|
||||||
|
|
||||||
|
toOpt :: CommandCheck
|
||||||
|
toOpt = CommandCheck 2 $ do
|
||||||
|
v <- Annex.getState Annex.toremote
|
||||||
|
unless (isNothing v) $ error "cannot use --to with this command"
|
||||||
|
|
||||||
|
dontCheck :: CommandCheck -> Command -> Command
|
||||||
|
dontCheck check cmd = mutateCheck cmd $ \c -> filter (/= check) c
|
||||||
|
|
||||||
|
addCheck :: Annex () -> Command -> Command
|
||||||
|
addCheck check cmd = mutateCheck cmd $
|
||||||
|
\c -> CommandCheck (length c + 100) check : c
|
||||||
|
|
||||||
|
mutateCheck :: Command -> ([CommandCheck] -> [CommandCheck]) -> Command
|
||||||
|
mutateCheck cmd@(Command { cmdcheck = c }) a = cmd { cmdcheck = a c }
|
106
CmdLine.hs
Normal file
106
CmdLine.hs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
{- git-annex command line parsing and dispatch
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module CmdLine (
|
||||||
|
dispatch,
|
||||||
|
usage,
|
||||||
|
shutdown
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified System.IO.Error as IO
|
||||||
|
import qualified Control.Exception as E
|
||||||
|
import Control.Exception (throw)
|
||||||
|
import System.Console.GetOpt
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Annex.Queue
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Command
|
||||||
|
import Annex.Content
|
||||||
|
import Command
|
||||||
|
|
||||||
|
type Params = [String]
|
||||||
|
type Flags = [Annex ()]
|
||||||
|
|
||||||
|
{- Runs the passed command line. -}
|
||||||
|
dispatch :: Params -> [Command] -> [Option] -> String -> IO Git.Repo -> IO ()
|
||||||
|
dispatch args cmds options header getgitrepo = do
|
||||||
|
setupConsole
|
||||||
|
r <- E.try getgitrepo :: IO (Either E.SomeException Git.Repo)
|
||||||
|
case r of
|
||||||
|
Left e -> fromMaybe (throw e) (cmdnorepo cmd)
|
||||||
|
Right g -> do
|
||||||
|
state <- Annex.new g
|
||||||
|
(actions, state') <- Annex.run state $ do
|
||||||
|
sequence_ flags
|
||||||
|
prepCommand cmd params
|
||||||
|
tryRun state' cmd $ [startup] ++ actions ++ [shutdown]
|
||||||
|
where
|
||||||
|
(flags, cmd, params) = parseCmd args cmds options header
|
||||||
|
|
||||||
|
{- Parses command line, and returns actions to run to configure flags,
|
||||||
|
- the Command being run, and the remaining parameters for the command. -}
|
||||||
|
parseCmd :: Params -> [Command] -> [Option] -> String -> (Flags, Command, Params)
|
||||||
|
parseCmd argv cmds options header = check $ getOpt Permute options argv
|
||||||
|
where
|
||||||
|
check (_, [], []) = err "missing command"
|
||||||
|
check (flags, name:rest, [])
|
||||||
|
| null matches = err $ "unknown command " ++ name
|
||||||
|
| otherwise = (flags, Prelude.head matches, rest)
|
||||||
|
where
|
||||||
|
matches = filter (\c -> name == cmdname c) cmds
|
||||||
|
check (_, _, errs) = err $ concat errs
|
||||||
|
err msg = error $ msg ++ "\n\n" ++ usage header cmds options
|
||||||
|
|
||||||
|
{- Usage message with lists of commands and options. -}
|
||||||
|
usage :: String -> [Command] -> [Option] -> String
|
||||||
|
usage header cmds options = usageInfo top options ++ commands
|
||||||
|
where
|
||||||
|
top = header ++ "\n\nOptions:"
|
||||||
|
commands = "\nCommands:\n" ++ cmddescs
|
||||||
|
cmddescs = unlines $ map (indent . showcmd) cmds
|
||||||
|
showcmd c =
|
||||||
|
cmdname c ++
|
||||||
|
pad (longest cmdname + 1) (cmdname c) ++
|
||||||
|
cmdparams c ++
|
||||||
|
pad (longest cmdparams + 2) (cmdparams c) ++
|
||||||
|
cmddesc c
|
||||||
|
pad n s = replicate (n - length s) ' '
|
||||||
|
longest f = foldl max 0 $ map (length . f) cmds
|
||||||
|
|
||||||
|
{- Runs a list of Annex actions. Catches IO errors and continues
|
||||||
|
- (but explicitly thrown errors terminate the whole command).
|
||||||
|
-}
|
||||||
|
tryRun :: Annex.AnnexState -> Command -> [CommandCleanup] -> IO ()
|
||||||
|
tryRun = tryRun' 0
|
||||||
|
tryRun' :: Integer -> Annex.AnnexState -> Command -> [CommandCleanup] -> IO ()
|
||||||
|
tryRun' errnum _ cmd []
|
||||||
|
| errnum > 0 = error $ cmdname cmd ++ ": " ++ show errnum ++ " failed"
|
||||||
|
| otherwise = return ()
|
||||||
|
tryRun' errnum state cmd (a:as) = run >>= handle
|
||||||
|
where
|
||||||
|
run = IO.try $ Annex.run state $ do
|
||||||
|
Annex.Queue.flushWhenFull
|
||||||
|
a
|
||||||
|
handle (Left err) = showerr err >> cont False state
|
||||||
|
handle (Right (success, state')) = cont success state'
|
||||||
|
cont success s = tryRun' (if success then errnum else errnum + 1) s cmd as
|
||||||
|
showerr err = Annex.eval state $ do
|
||||||
|
showErr err
|
||||||
|
showEndFail
|
||||||
|
|
||||||
|
{- Actions to perform each time ran. -}
|
||||||
|
startup :: Annex Bool
|
||||||
|
startup = return True
|
||||||
|
|
||||||
|
{- Cleanup actions. -}
|
||||||
|
shutdown :: Annex Bool
|
||||||
|
shutdown = do
|
||||||
|
saveState
|
||||||
|
liftIO Git.Command.reap -- zombies from long-running git processes
|
||||||
|
return True
|
108
Command.hs
Normal file
108
Command.hs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
{- git-annex command infrastructure
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command (
|
||||||
|
command,
|
||||||
|
noRepo,
|
||||||
|
next,
|
||||||
|
stop,
|
||||||
|
stopUnless,
|
||||||
|
prepCommand,
|
||||||
|
doCommand,
|
||||||
|
whenAnnexed,
|
||||||
|
ifAnnexed,
|
||||||
|
notBareRepo,
|
||||||
|
isBareRepo,
|
||||||
|
autoCopies,
|
||||||
|
module ReExported
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Backend
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Git
|
||||||
|
import Types.Command as ReExported
|
||||||
|
import Seek as ReExported
|
||||||
|
import Checks as ReExported
|
||||||
|
import Options as ReExported
|
||||||
|
import Logs.Trust
|
||||||
|
import Logs.Location
|
||||||
|
import Config
|
||||||
|
|
||||||
|
{- Generates a normal command -}
|
||||||
|
command :: String -> String -> [CommandSeek] -> String -> Command
|
||||||
|
command = Command Nothing commonChecks
|
||||||
|
|
||||||
|
{- Adds a fallback action to a command, that will be run if it's used
|
||||||
|
- outside a git repository. -}
|
||||||
|
noRepo :: IO () -> Command -> Command
|
||||||
|
noRepo a c = c { cmdnorepo = Just a }
|
||||||
|
|
||||||
|
{- For start and perform stages to indicate what step to run next. -}
|
||||||
|
next :: a -> Annex (Maybe a)
|
||||||
|
next a = return $ Just a
|
||||||
|
|
||||||
|
{- Or to indicate nothing needs to be done. -}
|
||||||
|
stop :: Annex (Maybe a)
|
||||||
|
stop = return Nothing
|
||||||
|
|
||||||
|
{- Stops unless a condition is met. -}
|
||||||
|
stopUnless :: Annex Bool -> Annex (Maybe a) -> Annex (Maybe a)
|
||||||
|
stopUnless c a = do
|
||||||
|
ok <- c
|
||||||
|
if ok then a else stop
|
||||||
|
|
||||||
|
{- Prepares to run a command via the check and seek stages, returning a
|
||||||
|
- list of actions to perform to run the command. -}
|
||||||
|
prepCommand :: Command -> [String] -> Annex [CommandCleanup]
|
||||||
|
prepCommand Command { cmdseek = seek, cmdcheck = c } params = do
|
||||||
|
mapM_ runCheck c
|
||||||
|
map doCommand . concat <$> mapM (\s -> s params) seek
|
||||||
|
|
||||||
|
{- Runs a command through the start, perform and cleanup stages -}
|
||||||
|
doCommand :: CommandStart -> CommandCleanup
|
||||||
|
doCommand = start
|
||||||
|
where
|
||||||
|
start = stage $ maybe skip perform
|
||||||
|
perform = stage $ maybe failure cleanup
|
||||||
|
cleanup = stage $ status
|
||||||
|
stage = (=<<)
|
||||||
|
skip = return True
|
||||||
|
failure = showEndFail >> return False
|
||||||
|
status r = showEndResult r >> return r
|
||||||
|
|
||||||
|
{- Modifies an action to only act on files that are already annexed,
|
||||||
|
- and passes the key and backend on to it. -}
|
||||||
|
whenAnnexed :: (FilePath -> (Key, Backend Annex) -> Annex (Maybe a)) -> FilePath -> Annex (Maybe a)
|
||||||
|
whenAnnexed a file = ifAnnexed file (a file) (return Nothing)
|
||||||
|
|
||||||
|
ifAnnexed :: FilePath -> ((Key, Backend Annex) -> Annex a) -> Annex a -> Annex a
|
||||||
|
ifAnnexed file yes no = maybe no yes =<< Backend.lookupFile file
|
||||||
|
|
||||||
|
notBareRepo :: Annex a -> Annex a
|
||||||
|
notBareRepo a = do
|
||||||
|
whenM isBareRepo $
|
||||||
|
error "You cannot run this subcommand in a bare repository."
|
||||||
|
a
|
||||||
|
|
||||||
|
isBareRepo :: Annex Bool
|
||||||
|
isBareRepo = fromRepo Git.repoIsLocalBare
|
||||||
|
|
||||||
|
{- Used for commands that have an auto mode that checks the number of known
|
||||||
|
- copies of a key.
|
||||||
|
-
|
||||||
|
- In auto mode, first checks that the number of known
|
||||||
|
- copies of the key is > or < than the numcopies setting, before running
|
||||||
|
- the action. -}
|
||||||
|
autoCopies :: Key -> (Int -> Int -> Bool) -> Maybe Int -> CommandStart -> CommandStart
|
||||||
|
autoCopies key vs numcopiesattr a = Annex.getState Annex.auto >>= auto
|
||||||
|
where
|
||||||
|
auto False = a
|
||||||
|
auto True = do
|
||||||
|
needed <- getNumCopies numcopiesattr
|
||||||
|
(_, have) <- trustPartition UnTrusted =<< keyLocations key
|
||||||
|
if length have `vs` needed then a else stop
|
93
Command/Add.hs
Normal file
93
Command/Add.hs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Add where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Annex.Exception
|
||||||
|
import Command
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Annex.Queue
|
||||||
|
import qualified Backend
|
||||||
|
import Logs.Location
|
||||||
|
import Annex.Content
|
||||||
|
import Utility.Touch
|
||||||
|
import Backend
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "add" paramPaths seek "add files to annex"]
|
||||||
|
|
||||||
|
{- Add acts on both files not checked into git yet, and unlocked files. -}
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesNotInGit start, withFilesUnlocked start]
|
||||||
|
|
||||||
|
{- The add subcommand annexes a file, storing it in a backend, and then
|
||||||
|
- moving it into the annex directory and setting up the symlink pointing
|
||||||
|
- to its content. -}
|
||||||
|
start :: BackendFile -> CommandStart
|
||||||
|
start p@(_, file) = notBareRepo $ ifAnnexed file fixup add
|
||||||
|
where
|
||||||
|
add = do
|
||||||
|
s <- liftIO $ getSymbolicLinkStatus file
|
||||||
|
if isSymbolicLink s || not (isRegularFile s)
|
||||||
|
then stop
|
||||||
|
else do
|
||||||
|
showStart "add" file
|
||||||
|
next $ perform p
|
||||||
|
fixup (key, _) = do
|
||||||
|
-- fixup from an interrupted add; the symlink
|
||||||
|
-- is present but not yet added to git
|
||||||
|
showStart "add" file
|
||||||
|
liftIO $ removeFile file
|
||||||
|
next $ next $ cleanup file key =<< inAnnex key
|
||||||
|
|
||||||
|
perform :: BackendFile -> CommandPerform
|
||||||
|
perform (backend, file) = Backend.genKey file backend >>= go
|
||||||
|
where
|
||||||
|
go Nothing = stop
|
||||||
|
go (Just (key, _)) = do
|
||||||
|
handle (undo file key) $ moveAnnex key file
|
||||||
|
next $ cleanup file key True
|
||||||
|
|
||||||
|
{- On error, put the file back so it doesn't seem to have vanished.
|
||||||
|
- This can be called before or after the symlink is in place. -}
|
||||||
|
undo :: FilePath -> Key -> IOException -> Annex a
|
||||||
|
undo file key e = do
|
||||||
|
unlessM (inAnnex key) rethrow -- no cleanup to do
|
||||||
|
liftIO $ whenM (doesFileExist file) $ removeFile file
|
||||||
|
handle tryharder $ fromAnnex key file
|
||||||
|
logStatus key InfoMissing
|
||||||
|
rethrow
|
||||||
|
where
|
||||||
|
rethrow = throw e
|
||||||
|
|
||||||
|
-- fromAnnex could fail if the file ownership is weird
|
||||||
|
tryharder :: IOException -> Annex ()
|
||||||
|
tryharder _ = do
|
||||||
|
src <- inRepo $ gitAnnexLocation key
|
||||||
|
liftIO $ moveFile src file
|
||||||
|
|
||||||
|
cleanup :: FilePath -> Key -> Bool -> CommandCleanup
|
||||||
|
cleanup file key hascontent = do
|
||||||
|
handle (undo file key) $ do
|
||||||
|
link <- calcGitLink file key
|
||||||
|
liftIO $ createSymbolicLink link file
|
||||||
|
|
||||||
|
when hascontent $ do
|
||||||
|
logStatus key InfoPresent
|
||||||
|
|
||||||
|
-- touch the symlink to have the same mtime as the
|
||||||
|
-- file it points to
|
||||||
|
liftIO $ do
|
||||||
|
mtime <- modificationTime <$> getFileStatus file
|
||||||
|
touch file (TimeSpec mtime) False
|
||||||
|
|
||||||
|
force <- Annex.getState Annex.force
|
||||||
|
if force
|
||||||
|
then Annex.Queue.add "add" [Param "-f", Param "--"] [file]
|
||||||
|
else Annex.Queue.add "add" [Param "--"] [file]
|
||||||
|
return True
|
72
Command/AddUrl.hs
Normal file
72
Command/AddUrl.hs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.AddUrl where
|
||||||
|
|
||||||
|
import Network.URI
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Backend
|
||||||
|
import qualified Utility.Url as Url
|
||||||
|
import qualified Command.Add
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Backend.URL
|
||||||
|
import Annex.Content
|
||||||
|
import Logs.Web
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "addurl" (paramRepeating paramUrl) seek "add urls to annex"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withStrings start]
|
||||||
|
|
||||||
|
start :: String -> CommandStart
|
||||||
|
start s = notBareRepo $ go $ parseURI s
|
||||||
|
where
|
||||||
|
go Nothing = error $ "bad url " ++ s
|
||||||
|
go (Just url) = do
|
||||||
|
file <- liftIO $ url2file url
|
||||||
|
showStart "addurl" file
|
||||||
|
next $ perform s file
|
||||||
|
|
||||||
|
perform :: String -> FilePath -> CommandPerform
|
||||||
|
perform url file = do
|
||||||
|
fast <- Annex.getState Annex.fast
|
||||||
|
if fast then nodownload url file else download url file
|
||||||
|
|
||||||
|
download :: String -> FilePath -> CommandPerform
|
||||||
|
download url file = do
|
||||||
|
showAction $ "downloading " ++ url ++ " "
|
||||||
|
let dummykey = Backend.URL.fromUrl url
|
||||||
|
tmp <- fromRepo $ gitAnnexTmpLocation dummykey
|
||||||
|
liftIO $ createDirectoryIfMissing True (parentDir tmp)
|
||||||
|
stopUnless (liftIO $ Url.download url tmp) $ do
|
||||||
|
[(backend, _)] <- Backend.chooseBackends [file]
|
||||||
|
k <- Backend.genKey tmp backend
|
||||||
|
case k of
|
||||||
|
Nothing -> stop
|
||||||
|
Just (key, _) -> do
|
||||||
|
moveAnnex key tmp
|
||||||
|
setUrlPresent key url
|
||||||
|
next $ Command.Add.cleanup file key True
|
||||||
|
|
||||||
|
nodownload :: String -> FilePath -> CommandPerform
|
||||||
|
nodownload url file = do
|
||||||
|
let key = Backend.URL.fromUrl url
|
||||||
|
setUrlPresent key url
|
||||||
|
next $ Command.Add.cleanup file key False
|
||||||
|
|
||||||
|
url2file :: URI -> IO FilePath
|
||||||
|
url2file url = do
|
||||||
|
whenM (doesFileExist file) $
|
||||||
|
error $ "already have this url in " ++ file
|
||||||
|
return file
|
||||||
|
where
|
||||||
|
file = escape $ uriRegName auth ++ uriPath url ++ uriQuery url
|
||||||
|
escape = replace "/" "_" . replace "?" "_"
|
||||||
|
auth = fromMaybe (error $ "bad url " ++ show url) $ uriAuthority url
|
25
Command/ConfigList.hs
Normal file
25
Command/ConfigList.hs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.ConfigList where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Annex.UUID
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "configlist" paramNothing seek
|
||||||
|
"outputs relevant git configuration"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNothing start]
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
u <- getUUID
|
||||||
|
liftIO $ putStrLn $ "annex.uuid=" ++ fromUUID u
|
||||||
|
stop
|
26
Command/Copy.hs
Normal file
26
Command/Copy.hs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Copy where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Command.Move
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck toOpt $ dontCheck fromOpt $
|
||||||
|
command "copy" paramPaths seek
|
||||||
|
"copy content of files to/from another repository"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNumCopies $ \n -> whenAnnexed $ start n]
|
||||||
|
|
||||||
|
-- A copy is just a move that does not delete the source file.
|
||||||
|
-- However, --auto mode avoids unnecessary copies.
|
||||||
|
start :: Maybe Int -> FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start numcopies file (key, backend) = autoCopies key (<) numcopies $
|
||||||
|
Command.Move.start False file (key, backend)
|
32
Command/Dead.hs
Normal file
32
Command/Dead.hs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Dead where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import Logs.Trust
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "dead" (paramRepeating paramRemote) seek
|
||||||
|
"hide a lost repository"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start ws = do
|
||||||
|
let name = unwords ws
|
||||||
|
showStart "dead " name
|
||||||
|
u <- Remote.nameToUUID name
|
||||||
|
next $ perform u
|
||||||
|
|
||||||
|
perform :: UUID -> CommandPerform
|
||||||
|
perform uuid = do
|
||||||
|
trustSet uuid DeadTrusted
|
||||||
|
next $ return True
|
32
Command/Describe.hs
Normal file
32
Command/Describe.hs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Describe where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import Logs.UUID
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "describe" (paramPair paramRemote paramDesc) seek
|
||||||
|
"change description of a repository"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start (name:description) = do
|
||||||
|
showStart "describe" name
|
||||||
|
u <- Remote.nameToUUID name
|
||||||
|
next $ perform u $ unwords description
|
||||||
|
start _ = error "Specify a repository and a description."
|
||||||
|
|
||||||
|
perform :: UUID -> String -> CommandPerform
|
||||||
|
perform u description = do
|
||||||
|
describeUUID u description
|
||||||
|
next $ return True
|
132
Command/Drop.hs
Normal file
132
Command/Drop.hs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Drop where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Annex
|
||||||
|
import Annex.UUID
|
||||||
|
import Logs.Location
|
||||||
|
import Logs.Trust
|
||||||
|
import Annex.Content
|
||||||
|
import Config
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck fromOpt $ command "drop" paramPaths seek
|
||||||
|
"indicate content of files not currently wanted"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNumCopies $ \n -> whenAnnexed $ start n]
|
||||||
|
|
||||||
|
start :: Maybe Int -> FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start numcopies file (key, _) = autoCopies key (>) numcopies $ do
|
||||||
|
from <- Annex.getState Annex.fromremote
|
||||||
|
case from of
|
||||||
|
Nothing -> startLocal file numcopies key
|
||||||
|
Just name -> do
|
||||||
|
remote <- Remote.byName name
|
||||||
|
u <- getUUID
|
||||||
|
if Remote.uuid remote == u
|
||||||
|
then startLocal file numcopies key
|
||||||
|
else startRemote file numcopies key remote
|
||||||
|
|
||||||
|
startLocal :: FilePath -> Maybe Int -> Key -> CommandStart
|
||||||
|
startLocal file numcopies key = stopUnless (inAnnex key) $ do
|
||||||
|
showStart "drop" file
|
||||||
|
next $ performLocal key numcopies
|
||||||
|
|
||||||
|
startRemote :: FilePath -> Maybe Int -> Key -> Remote.Remote Annex -> CommandStart
|
||||||
|
startRemote file numcopies key remote = do
|
||||||
|
showStart "drop" file
|
||||||
|
next $ performRemote key numcopies remote
|
||||||
|
|
||||||
|
performLocal :: Key -> Maybe Int -> CommandPerform
|
||||||
|
performLocal key numcopies = lockContent key $ do
|
||||||
|
(remotes, trusteduuids) <- Remote.keyPossibilitiesTrusted key
|
||||||
|
untrusteduuids <- trustGet UnTrusted
|
||||||
|
let tocheck = Remote.remotesWithoutUUID remotes (trusteduuids++untrusteduuids)
|
||||||
|
stopUnless (canDropKey key numcopies trusteduuids tocheck []) $ do
|
||||||
|
whenM (inAnnex key) $ removeAnnex key
|
||||||
|
next $ cleanupLocal key
|
||||||
|
|
||||||
|
performRemote :: Key -> Maybe Int -> Remote.Remote Annex -> CommandPerform
|
||||||
|
performRemote key numcopies remote = lockContent key $ do
|
||||||
|
-- Filter the remote it's being dropped from out of the lists of
|
||||||
|
-- places assumed to have the key, and places to check.
|
||||||
|
-- When the local repo has the key, that's one additional copy.
|
||||||
|
(remotes, trusteduuids) <- Remote.keyPossibilitiesTrusted key
|
||||||
|
present <- inAnnex key
|
||||||
|
u <- getUUID
|
||||||
|
let have = filter (/= uuid) $
|
||||||
|
if present then u:trusteduuids else trusteduuids
|
||||||
|
untrusteduuids <- trustGet UnTrusted
|
||||||
|
let tocheck = filter (/= remote) $
|
||||||
|
Remote.remotesWithoutUUID remotes (have++untrusteduuids)
|
||||||
|
stopUnless (canDropKey key numcopies have tocheck [uuid]) $ do
|
||||||
|
ok <- Remote.removeKey remote key
|
||||||
|
next $ cleanupRemote key remote ok
|
||||||
|
where
|
||||||
|
uuid = Remote.uuid remote
|
||||||
|
|
||||||
|
cleanupLocal :: Key -> CommandCleanup
|
||||||
|
cleanupLocal key = do
|
||||||
|
logStatus key InfoMissing
|
||||||
|
return True
|
||||||
|
|
||||||
|
cleanupRemote :: Key -> Remote.Remote Annex -> Bool -> CommandCleanup
|
||||||
|
cleanupRemote key remote ok = do
|
||||||
|
-- better safe than sorry: assume the remote dropped the key
|
||||||
|
-- even if it seemed to fail; the failure could have occurred
|
||||||
|
-- after it really dropped it
|
||||||
|
Remote.logStatus remote key False
|
||||||
|
return ok
|
||||||
|
|
||||||
|
{- Checks specified remotes to verify that enough copies of a key exist to
|
||||||
|
- allow it to be safely removed (with no data loss). Can be provided with
|
||||||
|
- some locations where the key is known/assumed to be present. -}
|
||||||
|
canDropKey :: Key -> Maybe Int -> [UUID] -> [Remote.Remote Annex] -> [UUID] -> Annex Bool
|
||||||
|
canDropKey key numcopiesM have check skip = do
|
||||||
|
force <- Annex.getState Annex.force
|
||||||
|
if force || numcopiesM == Just 0
|
||||||
|
then return True
|
||||||
|
else do
|
||||||
|
need <- getNumCopies numcopiesM
|
||||||
|
findCopies key need skip have check
|
||||||
|
|
||||||
|
findCopies :: Key -> Int -> [UUID] -> [UUID] -> [Remote.Remote Annex] -> Annex Bool
|
||||||
|
findCopies key need skip = helper []
|
||||||
|
where
|
||||||
|
helper bad have []
|
||||||
|
| length have >= need = return True
|
||||||
|
| otherwise = notEnoughCopies key need have skip bad
|
||||||
|
helper bad have (r:rs)
|
||||||
|
| length have >= need = return True
|
||||||
|
| otherwise = do
|
||||||
|
let u = Remote.uuid r
|
||||||
|
let duplicate = u `elem` have
|
||||||
|
haskey <- Remote.hasKey r key
|
||||||
|
case (duplicate, haskey) of
|
||||||
|
(False, Right True) -> helper bad (u:have) rs
|
||||||
|
(False, Left _) -> helper (r:bad) have rs
|
||||||
|
_ -> helper bad have rs
|
||||||
|
|
||||||
|
notEnoughCopies :: Key -> Int -> [UUID] -> [UUID] -> [Remote.Remote Annex] -> Annex Bool
|
||||||
|
notEnoughCopies key need have skip bad = do
|
||||||
|
unsafe
|
||||||
|
showLongNote $
|
||||||
|
"Could only verify the existence of " ++
|
||||||
|
show (length have) ++ " out of " ++ show need ++
|
||||||
|
" necessary copies"
|
||||||
|
Remote.showTriedRemotes bad
|
||||||
|
Remote.showLocations key (have++skip)
|
||||||
|
hint
|
||||||
|
return False
|
||||||
|
where
|
||||||
|
unsafe = showNote "unsafe"
|
||||||
|
hint = showLongNote "(Use --force to override this check, or adjust annex.numcopies.)"
|
38
Command/DropKey.hs
Normal file
38
Command/DropKey.hs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.DropKey where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex
|
||||||
|
import Logs.Location
|
||||||
|
import Annex.Content
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "dropkey" (paramRepeating paramKey) seek
|
||||||
|
"drops annexed content for specified keys"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withKeys start]
|
||||||
|
|
||||||
|
start :: Key -> CommandStart
|
||||||
|
start key = stopUnless (inAnnex key) $ do
|
||||||
|
unlessM (Annex.getState Annex.force) $
|
||||||
|
error "dropkey can cause data loss; use --force if you're sure you want to do this"
|
||||||
|
showStart "dropkey" (show key)
|
||||||
|
next $ perform key
|
||||||
|
|
||||||
|
perform :: Key -> CommandPerform
|
||||||
|
perform key = lockContent key $ do
|
||||||
|
removeAnnex key
|
||||||
|
next $ cleanup key
|
||||||
|
|
||||||
|
cleanup :: Key -> CommandCleanup
|
||||||
|
cleanup key = do
|
||||||
|
logStatus key InfoMissing
|
||||||
|
return True
|
78
Command/DropUnused.hs
Normal file
78
Command/DropUnused.hs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.DropUnused where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Command.Drop
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Git
|
||||||
|
import Types.Key
|
||||||
|
|
||||||
|
type UnusedMap = M.Map String Key
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck fromOpt $ command "dropunused" (paramRepeating paramNumber)
|
||||||
|
seek "drop unused file content"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withUnusedMaps]
|
||||||
|
|
||||||
|
{- Read unused logs once, and pass the maps to each start action. -}
|
||||||
|
withUnusedMaps :: CommandSeek
|
||||||
|
withUnusedMaps params = do
|
||||||
|
unused <- readUnusedLog ""
|
||||||
|
unusedbad <- readUnusedLog "bad"
|
||||||
|
unusedtmp <- readUnusedLog "tmp"
|
||||||
|
return $ map (start (unused, unusedbad, unusedtmp)) params
|
||||||
|
|
||||||
|
start :: (UnusedMap, UnusedMap, UnusedMap) -> FilePath -> CommandStart
|
||||||
|
start (unused, unusedbad, unusedtmp) s = search
|
||||||
|
[ (unused, perform)
|
||||||
|
, (unusedbad, performOther gitAnnexBadLocation)
|
||||||
|
, (unusedtmp, performOther gitAnnexTmpLocation)
|
||||||
|
]
|
||||||
|
where
|
||||||
|
search [] = stop
|
||||||
|
search ((m, a):rest) =
|
||||||
|
case M.lookup s m of
|
||||||
|
Nothing -> search rest
|
||||||
|
Just key -> do
|
||||||
|
showStart "dropunused" s
|
||||||
|
next $ a key
|
||||||
|
|
||||||
|
perform :: Key -> CommandPerform
|
||||||
|
perform key = maybe droplocal dropremote =<< Annex.getState Annex.fromremote
|
||||||
|
where
|
||||||
|
dropremote name = do
|
||||||
|
r <- Remote.byName name
|
||||||
|
showAction $ "from " ++ Remote.name r
|
||||||
|
ok <- Remote.removeKey r key
|
||||||
|
next $ Command.Drop.cleanupRemote key r ok
|
||||||
|
droplocal = Command.Drop.performLocal key (Just 0) -- force drop
|
||||||
|
|
||||||
|
performOther :: (Key -> Git.Repo -> FilePath) -> Key -> CommandPerform
|
||||||
|
performOther filespec key = do
|
||||||
|
f <- fromRepo $ filespec key
|
||||||
|
liftIO $ whenM (doesFileExist f) $ removeFile f
|
||||||
|
next $ return True
|
||||||
|
|
||||||
|
readUnusedLog :: FilePath -> Annex UnusedMap
|
||||||
|
readUnusedLog prefix = do
|
||||||
|
f <- fromRepo $ gitAnnexUnusedLog prefix
|
||||||
|
e <- liftIO $ doesFileExist f
|
||||||
|
if e
|
||||||
|
then M.fromList . map parse . lines <$> liftIO (readFile f)
|
||||||
|
else return M.empty
|
||||||
|
where
|
||||||
|
parse line = (num, fromJust $ readKey rest)
|
||||||
|
where
|
||||||
|
(num, rest) = separate (== ' ') line
|
48
Command/Find.hs
Normal file
48
Command/Find.hs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Find where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Annex.Content
|
||||||
|
import Limit
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Utility.Format
|
||||||
|
import Utility.DataUnits
|
||||||
|
import Types.Key
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "find" paramPaths seek "lists available files"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesInGit $ whenAnnexed start]
|
||||||
|
|
||||||
|
start :: FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start file (key, _) = do
|
||||||
|
-- only files inAnnex are shown, unless the user has requested
|
||||||
|
-- others via a limit
|
||||||
|
whenM (liftM2 (||) limited (inAnnex key)) $
|
||||||
|
unlessM (showFullJSON vars) $ do
|
||||||
|
f <- Annex.getState Annex.format
|
||||||
|
case f of
|
||||||
|
Nothing -> liftIO $ putStrLn file
|
||||||
|
Just formatter -> liftIO $ putStr $
|
||||||
|
Utility.Format.format formatter $
|
||||||
|
M.fromList vars
|
||||||
|
stop
|
||||||
|
where
|
||||||
|
vars =
|
||||||
|
[ ("file", file)
|
||||||
|
, ("key", show key)
|
||||||
|
, ("backend", keyBackendName key)
|
||||||
|
, ("bytesize", size show)
|
||||||
|
, ("humansize", size $ roughSize storageUnits True)
|
||||||
|
]
|
||||||
|
size c = maybe "unknown" c $ keySize key
|
40
Command/Fix.hs
Normal file
40
Command/Fix.hs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Fix where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex.Queue
|
||||||
|
import Annex.Content
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "fix" paramPaths seek
|
||||||
|
"fix up symlinks to point to annexed content"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesInGit $ whenAnnexed start]
|
||||||
|
|
||||||
|
{- Fixes the symlink to an annexed file. -}
|
||||||
|
start :: FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start file (key, _) = do
|
||||||
|
link <- calcGitLink file key
|
||||||
|
stopUnless ((/=) link <$> liftIO (readSymbolicLink file)) $ do
|
||||||
|
showStart "fix" file
|
||||||
|
next $ perform file link
|
||||||
|
|
||||||
|
perform :: FilePath -> FilePath -> CommandPerform
|
||||||
|
perform file link = do
|
||||||
|
liftIO $ createDirectoryIfMissing True (parentDir file)
|
||||||
|
liftIO $ removeFile file
|
||||||
|
liftIO $ createSymbolicLink link file
|
||||||
|
next $ cleanup file
|
||||||
|
|
||||||
|
cleanup :: FilePath -> CommandCleanup
|
||||||
|
cleanup file = do
|
||||||
|
Annex.Queue.add "add" [Param "--force", Param "--"] [file]
|
||||||
|
return True
|
43
Command/FromKey.hs
Normal file
43
Command/FromKey.hs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.FromKey where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex.Queue
|
||||||
|
import Annex.Content
|
||||||
|
import Types.Key
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "fromkey" (paramPair paramKey paramPath) seek
|
||||||
|
"adds a file using a specific key"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start (keyname:file:[]) = notBareRepo $ do
|
||||||
|
let key = fromMaybe (error "bad key") $ readKey keyname
|
||||||
|
inbackend <- inAnnex key
|
||||||
|
unless inbackend $ error $
|
||||||
|
"key ("++ keyname ++") is not present in backend"
|
||||||
|
showStart "fromkey" file
|
||||||
|
next $ perform key file
|
||||||
|
start _ = error "specify a key and a dest file"
|
||||||
|
|
||||||
|
perform :: Key -> FilePath -> CommandPerform
|
||||||
|
perform key file = do
|
||||||
|
link <- calcGitLink file key
|
||||||
|
liftIO $ createDirectoryIfMissing True (parentDir file)
|
||||||
|
liftIO $ createSymbolicLink link file
|
||||||
|
next $ cleanup file
|
||||||
|
|
||||||
|
cleanup :: FilePath -> CommandCleanup
|
||||||
|
cleanup file = do
|
||||||
|
Annex.Queue.add "add" [Param "--"] [file]
|
||||||
|
return True
|
168
Command/Fsck.hs
Normal file
168
Command/Fsck.hs
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Fsck where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Types.Backend
|
||||||
|
import qualified Types.Key
|
||||||
|
import qualified Backend
|
||||||
|
import Annex.Content
|
||||||
|
import Logs.Location
|
||||||
|
import Logs.Trust
|
||||||
|
import Annex.UUID
|
||||||
|
import Utility.DataUnits
|
||||||
|
import Utility.FileMode
|
||||||
|
import Config
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "fsck" paramPaths seek "check for problems"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek =
|
||||||
|
[ withNumCopies $ \n -> whenAnnexed $ start n
|
||||||
|
, withBarePresentKeys startBare
|
||||||
|
]
|
||||||
|
|
||||||
|
start :: Maybe Int -> FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start numcopies file (key, backend) = do
|
||||||
|
showStart "fsck" file
|
||||||
|
next $ perform key file backend numcopies
|
||||||
|
|
||||||
|
perform :: Key -> FilePath -> Backend Annex -> Maybe Int -> CommandPerform
|
||||||
|
perform key file backend numcopies = check
|
||||||
|
-- order matters
|
||||||
|
[ verifyLocationLog key file
|
||||||
|
, checkKeySize key
|
||||||
|
, checkKeyNumCopies key file numcopies
|
||||||
|
, checkBackend backend key
|
||||||
|
]
|
||||||
|
|
||||||
|
{- To fsck a bare repository, fsck each key in the location log. -}
|
||||||
|
withBarePresentKeys :: (Key -> CommandStart) -> CommandSeek
|
||||||
|
withBarePresentKeys a params = isBareRepo >>= go
|
||||||
|
where
|
||||||
|
go False = return []
|
||||||
|
go True = do
|
||||||
|
unless (null params) $
|
||||||
|
error "fsck should be run without parameters in a bare repository"
|
||||||
|
prepStart a loggedKeys
|
||||||
|
|
||||||
|
startBare :: Key -> CommandStart
|
||||||
|
startBare key = case Backend.maybeLookupBackendName (Types.Key.keyBackendName key) of
|
||||||
|
Nothing -> stop
|
||||||
|
Just backend -> do
|
||||||
|
showStart "fsck" (show key)
|
||||||
|
next $ performBare key backend
|
||||||
|
|
||||||
|
{- Note that numcopies cannot be checked in a bare repository, because
|
||||||
|
- getting the numcopies value requires a working copy with .gitattributes
|
||||||
|
- files. -}
|
||||||
|
performBare :: Key -> Backend Annex -> CommandPerform
|
||||||
|
performBare key backend = check
|
||||||
|
[ verifyLocationLog key (show key)
|
||||||
|
, checkKeySize key
|
||||||
|
, checkBackend backend key
|
||||||
|
]
|
||||||
|
|
||||||
|
check :: [Annex Bool] -> CommandPerform
|
||||||
|
check = sequence >=> dispatch
|
||||||
|
where
|
||||||
|
dispatch vs
|
||||||
|
| all (== True) vs = next $ return True
|
||||||
|
| otherwise = stop
|
||||||
|
|
||||||
|
{- Checks that the location log reflects the current status of the key,
|
||||||
|
in this repository only. -}
|
||||||
|
verifyLocationLog :: Key -> String -> Annex Bool
|
||||||
|
verifyLocationLog key desc = do
|
||||||
|
present <- inAnnex key
|
||||||
|
|
||||||
|
-- Since we're checking that a key's file is present, throw
|
||||||
|
-- in a permission fixup here too.
|
||||||
|
when present $ do
|
||||||
|
f <- inRepo $ gitAnnexLocation key
|
||||||
|
liftIO $ do
|
||||||
|
preventWrite f
|
||||||
|
preventWrite (parentDir f)
|
||||||
|
|
||||||
|
u <- getUUID
|
||||||
|
uuids <- keyLocations key
|
||||||
|
|
||||||
|
case (present, u `elem` uuids) of
|
||||||
|
(True, False) -> do
|
||||||
|
fix u InfoPresent
|
||||||
|
-- There is no data loss, so do not fail.
|
||||||
|
return True
|
||||||
|
(False, True) -> do
|
||||||
|
fix u InfoMissing
|
||||||
|
warning $
|
||||||
|
"** Based on the location log, " ++ desc
|
||||||
|
++ "\n** was expected to be present, " ++
|
||||||
|
"but its content is missing."
|
||||||
|
return False
|
||||||
|
_ -> return True
|
||||||
|
|
||||||
|
where
|
||||||
|
fix u s = do
|
||||||
|
showNote "fixing location log"
|
||||||
|
logChange key u s
|
||||||
|
|
||||||
|
{- The size of the data for a key is checked against the size encoded in
|
||||||
|
- the key's metadata, if available. -}
|
||||||
|
checkKeySize :: Key -> Annex Bool
|
||||||
|
checkKeySize key = do
|
||||||
|
file <- inRepo $ gitAnnexLocation key
|
||||||
|
present <- liftIO $ doesFileExist file
|
||||||
|
case (present, Types.Key.keySize key) of
|
||||||
|
(_, Nothing) -> return True
|
||||||
|
(False, _) -> return True
|
||||||
|
(True, Just size) -> do
|
||||||
|
stat <- liftIO $ getFileStatus file
|
||||||
|
let size' = fromIntegral (fileSize stat)
|
||||||
|
if size == size'
|
||||||
|
then return True
|
||||||
|
else do
|
||||||
|
dest <- moveBad key
|
||||||
|
warning $ "Bad file size (" ++
|
||||||
|
compareSizes storageUnits True size size' ++
|
||||||
|
"); moved to " ++ dest
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
checkBackend :: Backend Annex -> Key -> Annex Bool
|
||||||
|
checkBackend = Types.Backend.fsckKey
|
||||||
|
|
||||||
|
checkKeyNumCopies :: Key -> FilePath -> Maybe Int -> Annex Bool
|
||||||
|
checkKeyNumCopies key file numcopies = do
|
||||||
|
needed <- getNumCopies numcopies
|
||||||
|
(untrustedlocations, safelocations) <- trustPartition UnTrusted =<< keyLocations key
|
||||||
|
let present = length safelocations
|
||||||
|
if present < needed
|
||||||
|
then do
|
||||||
|
ppuuids <- Remote.prettyPrintUUIDs "untrusted" untrustedlocations
|
||||||
|
warning $ missingNote file present needed ppuuids
|
||||||
|
return False
|
||||||
|
else return True
|
||||||
|
|
||||||
|
missingNote :: String -> Int -> Int -> String -> String
|
||||||
|
missingNote file 0 _ [] =
|
||||||
|
"** No known copies exist of " ++ file
|
||||||
|
missingNote file 0 _ untrusted =
|
||||||
|
"Only these untrusted locations may have copies of " ++ file ++
|
||||||
|
"\n" ++ untrusted ++
|
||||||
|
"Back it up to trusted locations with git-annex copy."
|
||||||
|
missingNote file present needed [] =
|
||||||
|
"Only " ++ show present ++ " of " ++ show needed ++
|
||||||
|
" trustworthy copies exist of " ++ file ++
|
||||||
|
"\nBack it up with git-annex copy."
|
||||||
|
missingNote file present needed untrusted =
|
||||||
|
missingNote file present needed [] ++
|
||||||
|
"\nThe following untrusted locations may also have copies: " ++
|
||||||
|
"\n" ++ untrusted
|
80
Command/Get.hs
Normal file
80
Command/Get.hs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Get where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Remote
|
||||||
|
import Annex.Content
|
||||||
|
import qualified Command.Move
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck fromOpt $ command "get" paramPaths seek
|
||||||
|
"make content of annexed files available"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNumCopies $ \n -> whenAnnexed $ start n]
|
||||||
|
|
||||||
|
start :: Maybe Int -> FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start numcopies file (key, _) = stopUnless (not <$> inAnnex key) $
|
||||||
|
autoCopies key (<) numcopies $ do
|
||||||
|
from <- Annex.getState Annex.fromremote
|
||||||
|
case from of
|
||||||
|
Nothing -> go $ perform key
|
||||||
|
Just name -> do
|
||||||
|
-- get --from = copy --from
|
||||||
|
src <- Remote.byName name
|
||||||
|
stopUnless (Command.Move.fromOk src key) $
|
||||||
|
go $ Command.Move.fromPerform src False key
|
||||||
|
where
|
||||||
|
go a = do
|
||||||
|
showStart "get" file
|
||||||
|
next a
|
||||||
|
|
||||||
|
perform :: Key -> CommandPerform
|
||||||
|
perform key = stopUnless (getViaTmp key $ getKeyFile key) $ do
|
||||||
|
next $ return True -- no cleanup needed
|
||||||
|
|
||||||
|
{- Try to find a copy of the file in one of the remotes,
|
||||||
|
- and copy it to here. -}
|
||||||
|
getKeyFile :: Key -> FilePath -> Annex Bool
|
||||||
|
getKeyFile key file = do
|
||||||
|
remotes <- Remote.keyPossibilities key
|
||||||
|
if null remotes
|
||||||
|
then do
|
||||||
|
showNote "not available"
|
||||||
|
Remote.showLocations key []
|
||||||
|
return False
|
||||||
|
else trycopy remotes remotes
|
||||||
|
where
|
||||||
|
trycopy full [] = do
|
||||||
|
Remote.showTriedRemotes full
|
||||||
|
Remote.showLocations key []
|
||||||
|
return False
|
||||||
|
trycopy full (r:rs) = do
|
||||||
|
probablythere <- probablyPresent r
|
||||||
|
if probablythere
|
||||||
|
then docopy r (trycopy full rs)
|
||||||
|
else trycopy full rs
|
||||||
|
-- This check is to avoid an ugly message if a remote is a
|
||||||
|
-- drive that is not mounted.
|
||||||
|
probablyPresent r =
|
||||||
|
if Remote.hasKeyCheap r
|
||||||
|
then do
|
||||||
|
res <- Remote.hasKey r key
|
||||||
|
case res of
|
||||||
|
Right b -> return b
|
||||||
|
Left _ -> return False
|
||||||
|
else return True
|
||||||
|
docopy r continue = do
|
||||||
|
showAction $ "from " ++ Remote.name r
|
||||||
|
copied <- Remote.retrieveKeyFile r key file
|
||||||
|
if copied
|
||||||
|
then return True
|
||||||
|
else continue
|
27
Command/InAnnex.hs
Normal file
27
Command/InAnnex.hs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.InAnnex where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Annex.Content
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "inannex" (paramRepeating paramKey) seek
|
||||||
|
"checks if keys are present in the annex"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withKeys start]
|
||||||
|
|
||||||
|
start :: Key -> CommandStart
|
||||||
|
start key = inAnnexSafe key >>= dispatch
|
||||||
|
where
|
||||||
|
dispatch (Just True) = stop
|
||||||
|
dispatch (Just False) = exit 1
|
||||||
|
dispatch Nothing = exit 100
|
||||||
|
exit n = liftIO $ exitWith $ ExitFailure n
|
31
Command/Init.hs
Normal file
31
Command/Init.hs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Init where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Init
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck repoExists $
|
||||||
|
command "init" paramDesc seek "initialize git-annex"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start ws = do
|
||||||
|
showStart "init" description
|
||||||
|
next $ perform description
|
||||||
|
where
|
||||||
|
description = unwords ws
|
||||||
|
|
||||||
|
perform :: String -> CommandPerform
|
||||||
|
perform description = do
|
||||||
|
initialize $ if null description then Nothing else Just description
|
||||||
|
next $ return True
|
95
Command/InitRemote.hs
Normal file
95
Command/InitRemote.hs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.InitRemote where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Logs.Remote
|
||||||
|
import qualified Types.Remote as R
|
||||||
|
import Annex.UUID
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "initremote"
|
||||||
|
(paramPair paramName $ paramOptional $ paramRepeating paramKeyValue)
|
||||||
|
seek "sets up a special (non-git) remote"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start [] = do
|
||||||
|
names <- remoteNames
|
||||||
|
error $ "Specify a name for the remote. " ++
|
||||||
|
if null names
|
||||||
|
then ""
|
||||||
|
else "Either a new name, or one of these existing special remotes: " ++ join " " names
|
||||||
|
start (name:ws) = do
|
||||||
|
(u, c) <- findByName name
|
||||||
|
let fullconfig = config `M.union` c
|
||||||
|
t <- findType fullconfig
|
||||||
|
|
||||||
|
showStart "initremote" name
|
||||||
|
next $ perform t u $ M.union config c
|
||||||
|
|
||||||
|
where
|
||||||
|
config = Logs.Remote.keyValToConfig ws
|
||||||
|
|
||||||
|
perform :: R.RemoteType Annex -> UUID -> R.RemoteConfig -> CommandPerform
|
||||||
|
perform t u c = do
|
||||||
|
c' <- R.setup t u c
|
||||||
|
next $ cleanup u c'
|
||||||
|
|
||||||
|
cleanup :: UUID -> R.RemoteConfig -> CommandCleanup
|
||||||
|
cleanup u c = do
|
||||||
|
Logs.Remote.configSet u c
|
||||||
|
return True
|
||||||
|
|
||||||
|
{- Look up existing remote's UUID and config by name, or generate a new one -}
|
||||||
|
findByName :: String -> Annex (UUID, R.RemoteConfig)
|
||||||
|
findByName name = do
|
||||||
|
m <- Logs.Remote.readRemoteLog
|
||||||
|
maybe generate return $ findByName' name m
|
||||||
|
where
|
||||||
|
generate = do
|
||||||
|
uuid <- liftIO genUUID
|
||||||
|
return (uuid, M.insert nameKey name M.empty)
|
||||||
|
|
||||||
|
findByName' :: String -> M.Map UUID R.RemoteConfig -> Maybe (UUID, R.RemoteConfig)
|
||||||
|
findByName' n = headMaybe . filter (matching . snd) . M.toList
|
||||||
|
where
|
||||||
|
matching c = case M.lookup nameKey c of
|
||||||
|
Nothing -> False
|
||||||
|
Just n'
|
||||||
|
| n' == n -> True
|
||||||
|
| otherwise -> False
|
||||||
|
|
||||||
|
remoteNames :: Annex [String]
|
||||||
|
remoteNames = do
|
||||||
|
m <- Logs.Remote.readRemoteLog
|
||||||
|
return $ mapMaybe (M.lookup nameKey . snd) $ M.toList m
|
||||||
|
|
||||||
|
{- find the specified remote type -}
|
||||||
|
findType :: R.RemoteConfig -> Annex (R.RemoteType Annex)
|
||||||
|
findType config = maybe unspecified specified $ M.lookup typeKey config
|
||||||
|
where
|
||||||
|
unspecified = error "Specify the type of remote with type="
|
||||||
|
specified s = case filter (findtype s) Remote.remoteTypes of
|
||||||
|
[] -> error $ "Unknown remote type " ++ s
|
||||||
|
(t:_) -> return t
|
||||||
|
findtype s i = R.typename i == s
|
||||||
|
|
||||||
|
{- The name of a configured remote is stored in its config using this key. -}
|
||||||
|
nameKey :: String
|
||||||
|
nameKey = "name"
|
||||||
|
|
||||||
|
{- The type of a remote is stored in its config using this key. -}
|
||||||
|
typeKey :: String
|
||||||
|
typeKey = "type"
|
34
Command/Lock.hs
Normal file
34
Command/Lock.hs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Lock where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex.Queue
|
||||||
|
import Backend
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "lock" paramPaths seek "undo unlock command"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesUnlocked start, withFilesUnlockedToBeCommitted start]
|
||||||
|
|
||||||
|
{- Undo unlock -}
|
||||||
|
start :: BackendFile -> CommandStart
|
||||||
|
start (_, file) = do
|
||||||
|
showStart "lock" file
|
||||||
|
next $ perform file
|
||||||
|
|
||||||
|
perform :: FilePath -> CommandPerform
|
||||||
|
perform file = do
|
||||||
|
liftIO $ removeFile file
|
||||||
|
-- Checkout from HEAD to get rid of any changes that might be
|
||||||
|
-- staged in the index, and get back to the previous symlink to
|
||||||
|
-- the content.
|
||||||
|
Annex.Queue.add "checkout" [Param "HEAD", Param "--"] [file]
|
||||||
|
next $ return True -- no cleanup needed
|
238
Command/Map.hs
Normal file
238
Command/Map.hs
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Map where
|
||||||
|
|
||||||
|
import Control.Exception.Extensible
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Url
|
||||||
|
import qualified Git.Config
|
||||||
|
import qualified Git.Construct
|
||||||
|
import qualified Annex
|
||||||
|
import Annex.UUID
|
||||||
|
import Logs.UUID
|
||||||
|
import Logs.Trust
|
||||||
|
import Annex.Ssh
|
||||||
|
import qualified Utility.Dot as Dot
|
||||||
|
|
||||||
|
-- a link from the first repository to the second (its remote)
|
||||||
|
data Link = Link Git.Repo Git.Repo
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck repoExists $
|
||||||
|
command "map" paramNothing seek "generate map of repositories"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNothing start]
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
rs <- spider =<< gitRepo
|
||||||
|
|
||||||
|
umap <- uuidMap
|
||||||
|
trusted <- trustGet Trusted
|
||||||
|
|
||||||
|
liftIO $ writeFile file (drawMap rs umap trusted)
|
||||||
|
next $ next $ do
|
||||||
|
fast <- Annex.getState Annex.fast
|
||||||
|
if fast
|
||||||
|
then return True
|
||||||
|
else do
|
||||||
|
showLongNote $ "running: dot -Tx11 " ++ file
|
||||||
|
showOutput
|
||||||
|
liftIO $ boolSystem "dot" [Param "-Tx11", File file]
|
||||||
|
where
|
||||||
|
file = "map.dot"
|
||||||
|
|
||||||
|
{- Generates a graph for dot(1). Each repository, and any other uuids, are
|
||||||
|
- displayed as a node, and each of its remotes is represented as an edge
|
||||||
|
- pointing at the node for the remote.
|
||||||
|
-
|
||||||
|
- The order nodes are added to the graph matters, since dot will draw
|
||||||
|
- the first ones near to the top and left. So it looks better to put
|
||||||
|
- the repositories first, followed by uuids that were not matched
|
||||||
|
- to a repository.
|
||||||
|
-}
|
||||||
|
drawMap :: [Git.Repo] -> M.Map UUID String -> [UUID] -> String
|
||||||
|
drawMap rs umap ts = Dot.graph $ repos ++ trusted ++ others
|
||||||
|
where
|
||||||
|
repos = map (node umap rs) rs
|
||||||
|
ruuids = ts ++ map getUncachedUUID rs
|
||||||
|
others = map (unreachable . uuidnode) $
|
||||||
|
filter (`notElem` ruuids) (M.keys umap)
|
||||||
|
trusted = map (trustworthy . uuidnode) ts
|
||||||
|
uuidnode u = Dot.graphNode (fromUUID u) $
|
||||||
|
M.findWithDefault "" u umap
|
||||||
|
|
||||||
|
hostname :: Git.Repo -> String
|
||||||
|
hostname r
|
||||||
|
| Git.repoIsUrl r = Git.Url.host r
|
||||||
|
| otherwise = "localhost"
|
||||||
|
|
||||||
|
basehostname :: Git.Repo -> String
|
||||||
|
basehostname r = Prelude.head $ split "." $ hostname r
|
||||||
|
|
||||||
|
{- A name to display for a repo. Uses the name from uuid.log if available,
|
||||||
|
- or the remote name if not. -}
|
||||||
|
repoName :: M.Map UUID String -> Git.Repo -> String
|
||||||
|
repoName umap r
|
||||||
|
| repouuid == NoUUID = fallback
|
||||||
|
| otherwise = M.findWithDefault fallback repouuid umap
|
||||||
|
where
|
||||||
|
repouuid = getUncachedUUID r
|
||||||
|
fallback = fromMaybe "unknown" $ Git.remoteName r
|
||||||
|
|
||||||
|
{- A unique id for the node for a repo. Uses the annex.uuid if available. -}
|
||||||
|
nodeId :: Git.Repo -> String
|
||||||
|
nodeId r =
|
||||||
|
case getUncachedUUID r of
|
||||||
|
NoUUID -> Git.repoLocation r
|
||||||
|
UUID u -> u
|
||||||
|
|
||||||
|
{- A node representing a repo. -}
|
||||||
|
node :: M.Map UUID String -> [Git.Repo] -> Git.Repo -> String
|
||||||
|
node umap fullinfo r = unlines $ n:edges
|
||||||
|
where
|
||||||
|
n = Dot.subGraph (hostname r) (basehostname r) "lightblue" $
|
||||||
|
decorate $ Dot.graphNode (nodeId r) (repoName umap r)
|
||||||
|
edges = map (edge umap fullinfo r) (Git.remotes r)
|
||||||
|
decorate
|
||||||
|
| Git.config r == M.empty = unreachable
|
||||||
|
| otherwise = reachable
|
||||||
|
|
||||||
|
{- An edge between two repos. The second repo is a remote of the first. -}
|
||||||
|
edge :: M.Map UUID String -> [Git.Repo] -> Git.Repo -> Git.Repo -> String
|
||||||
|
edge umap fullinfo from to =
|
||||||
|
Dot.graphEdge (nodeId from) (nodeId fullto) edgename
|
||||||
|
where
|
||||||
|
-- get the full info for the remote, to get its UUID
|
||||||
|
fullto = findfullinfo to
|
||||||
|
findfullinfo n =
|
||||||
|
case filter (same n) fullinfo of
|
||||||
|
[] -> n
|
||||||
|
(n':_) -> n'
|
||||||
|
{- Only name an edge if the name is different than the name
|
||||||
|
- that will be used for the destination node, and is
|
||||||
|
- different from its hostname. (This reduces visual clutter.) -}
|
||||||
|
edgename = maybe Nothing calcname $ Git.remoteName to
|
||||||
|
calcname n
|
||||||
|
| n `elem` [repoName umap fullto, hostname fullto] = Nothing
|
||||||
|
| otherwise = Just n
|
||||||
|
|
||||||
|
unreachable :: String -> String
|
||||||
|
unreachable = Dot.fillColor "red"
|
||||||
|
reachable :: String -> String
|
||||||
|
reachable = Dot.fillColor "white"
|
||||||
|
trustworthy :: String -> String
|
||||||
|
trustworthy = Dot.fillColor "green"
|
||||||
|
|
||||||
|
{- Recursively searches out remotes starting with the specified repo. -}
|
||||||
|
spider :: Git.Repo -> Annex [Git.Repo]
|
||||||
|
spider r = spider' [r] []
|
||||||
|
spider' :: [Git.Repo] -> [Git.Repo] -> Annex [Git.Repo]
|
||||||
|
spider' [] known = return known
|
||||||
|
spider' (r:rs) known
|
||||||
|
| any (same r) known = spider' rs known
|
||||||
|
| otherwise = do
|
||||||
|
r' <- scan r
|
||||||
|
|
||||||
|
-- The remotes will be relative to r', and need to be
|
||||||
|
-- made absolute for later use.
|
||||||
|
remotes <- mapM (absRepo r') (Git.remotes r')
|
||||||
|
let r'' = r' { Git.remotes = remotes }
|
||||||
|
|
||||||
|
spider' (rs ++ remotes) (r'':known)
|
||||||
|
|
||||||
|
{- Converts repos to a common absolute form. -}
|
||||||
|
absRepo :: Git.Repo -> Git.Repo -> Annex Git.Repo
|
||||||
|
absRepo reference r
|
||||||
|
| Git.repoIsUrl reference = return $ Git.Construct.localToUrl reference r
|
||||||
|
| otherwise = liftIO $ Git.Construct.fromAbsPath =<< absPath (Git.workTree r)
|
||||||
|
|
||||||
|
{- Checks if two repos are the same. -}
|
||||||
|
same :: Git.Repo -> Git.Repo -> Bool
|
||||||
|
same a b
|
||||||
|
| both Git.repoIsSsh = matching Git.Url.authority && matching Git.workTree
|
||||||
|
| both Git.repoIsUrl && neither Git.repoIsSsh = matching show
|
||||||
|
| neither Git.repoIsSsh = matching Git.workTree
|
||||||
|
| otherwise = False
|
||||||
|
|
||||||
|
where
|
||||||
|
matching t = t a == t b
|
||||||
|
both t = t a && t b
|
||||||
|
neither t = not (t a) && not (t b)
|
||||||
|
|
||||||
|
{- reads the config of a remote, with progress display -}
|
||||||
|
scan :: Git.Repo -> Annex Git.Repo
|
||||||
|
scan r = do
|
||||||
|
showStart "map" $ Git.repoDescribe r
|
||||||
|
v <- tryScan r
|
||||||
|
case v of
|
||||||
|
Just r' -> do
|
||||||
|
showEndOk
|
||||||
|
return r'
|
||||||
|
Nothing -> do
|
||||||
|
showOutput
|
||||||
|
showEndFail
|
||||||
|
return r
|
||||||
|
|
||||||
|
{- tries to read the config of a remote, returning it only if it can
|
||||||
|
- be accessed -}
|
||||||
|
tryScan :: Git.Repo -> Annex (Maybe Git.Repo)
|
||||||
|
tryScan r
|
||||||
|
| Git.repoIsSsh r = sshscan
|
||||||
|
| Git.repoIsUrl r = return Nothing
|
||||||
|
| otherwise = safely $ Git.Config.read r
|
||||||
|
where
|
||||||
|
safely a = do
|
||||||
|
result <- liftIO (try a :: IO (Either SomeException Git.Repo))
|
||||||
|
case result of
|
||||||
|
Left _ -> return Nothing
|
||||||
|
Right r' -> return $ Just r'
|
||||||
|
pipedconfig cmd params = safely $
|
||||||
|
pOpen ReadFromPipe cmd (toCommand params) $
|
||||||
|
Git.Config.hRead r
|
||||||
|
|
||||||
|
configlist =
|
||||||
|
onRemote r (pipedconfig, Nothing) "configlist" []
|
||||||
|
manualconfiglist = do
|
||||||
|
sshparams <- sshToRepo r [Param sshcmd]
|
||||||
|
liftIO $ pipedconfig "ssh" sshparams
|
||||||
|
where
|
||||||
|
sshcmd = cddir ++ " && " ++
|
||||||
|
"git config --null --list"
|
||||||
|
dir = Git.workTree r
|
||||||
|
cddir
|
||||||
|
| "/~" `isPrefixOf` dir =
|
||||||
|
let (userhome, reldir) = span (/= '/') (drop 1 dir)
|
||||||
|
in "cd " ++ userhome ++ " && cd " ++ shellEscape (drop 1 reldir)
|
||||||
|
| otherwise = "cd " ++ shellEscape dir
|
||||||
|
|
||||||
|
-- First, try sshing and running git config manually,
|
||||||
|
-- only fall back to git-annex-shell configlist if that
|
||||||
|
-- fails.
|
||||||
|
--
|
||||||
|
-- This is done for two reasons, first I'd like this
|
||||||
|
-- subcommand to be usable on non-git-annex repos.
|
||||||
|
-- Secondly, configlist doesn't include information about
|
||||||
|
-- the remote's remotes.
|
||||||
|
sshscan = do
|
||||||
|
sshnote
|
||||||
|
v <- manualconfiglist
|
||||||
|
case v of
|
||||||
|
Nothing -> do
|
||||||
|
sshnote
|
||||||
|
configlist
|
||||||
|
ok -> return ok
|
||||||
|
|
||||||
|
sshnote = do
|
||||||
|
showAction "sshing"
|
||||||
|
showOutput
|
29
Command/Merge.hs
Normal file
29
Command/Merge.hs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Merge where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex.Branch
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "merge" paramNothing seek
|
||||||
|
"auto-merge remote changes into git-annex branch"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNothing start]
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
showStart "merge" "."
|
||||||
|
next perform
|
||||||
|
|
||||||
|
perform :: CommandPerform
|
||||||
|
perform = do
|
||||||
|
Annex.Branch.update
|
||||||
|
next $ return True
|
79
Command/Migrate.hs
Normal file
79
Command/Migrate.hs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Migrate where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Backend
|
||||||
|
import qualified Types.Key
|
||||||
|
import Annex.Content
|
||||||
|
import qualified Command.Add
|
||||||
|
import Logs.Web
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "migrate" paramPaths seek "switch data to different backend"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withBackendFilesInGit $ \(b, f) -> whenAnnexed (start b) f]
|
||||||
|
|
||||||
|
start :: Maybe (Backend Annex) -> FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start b file (key, oldbackend) = do
|
||||||
|
exists <- inAnnex key
|
||||||
|
newbackend <- choosebackend b
|
||||||
|
if (newbackend /= oldbackend || upgradableKey key) && exists
|
||||||
|
then do
|
||||||
|
showStart "migrate" file
|
||||||
|
next $ perform file key newbackend
|
||||||
|
else stop
|
||||||
|
where
|
||||||
|
choosebackend Nothing = Prelude.head <$> Backend.orderedList
|
||||||
|
choosebackend (Just backend) = return backend
|
||||||
|
|
||||||
|
{- Checks if a key is upgradable to a newer representation. -}
|
||||||
|
{- Ideally, all keys have file size metadata. Old keys may not. -}
|
||||||
|
upgradableKey :: Key -> Bool
|
||||||
|
upgradableKey key = isNothing $ Types.Key.keySize key
|
||||||
|
|
||||||
|
{- Store the old backend's key in the new backend
|
||||||
|
- The old backend's key is not dropped from it, because there may
|
||||||
|
- be other files still pointing at that key.
|
||||||
|
-
|
||||||
|
- Use the same filename as the file for the temp file name, to support
|
||||||
|
- backends that allow the filename to influence the keys they
|
||||||
|
- generate.
|
||||||
|
-}
|
||||||
|
perform :: FilePath -> Key -> Backend Annex -> CommandPerform
|
||||||
|
perform file oldkey newbackend = do
|
||||||
|
src <- inRepo $ gitAnnexLocation oldkey
|
||||||
|
tmp <- fromRepo gitAnnexTmpDir
|
||||||
|
let tmpfile = tmp </> takeFileName file
|
||||||
|
cleantmp tmpfile
|
||||||
|
liftIO $ createLink src tmpfile
|
||||||
|
k <- Backend.genKey tmpfile $ Just newbackend
|
||||||
|
cleantmp tmpfile
|
||||||
|
case k of
|
||||||
|
Nothing -> stop
|
||||||
|
Just (newkey, _) -> stopUnless (link src newkey) $ do
|
||||||
|
-- Update symlink to use the new key.
|
||||||
|
liftIO $ removeFile file
|
||||||
|
|
||||||
|
-- If the old key had some
|
||||||
|
-- associated urls, record them for
|
||||||
|
-- the new key as well.
|
||||||
|
urls <- getUrls oldkey
|
||||||
|
unless (null urls) $
|
||||||
|
mapM_ (setUrlPresent newkey) urls
|
||||||
|
|
||||||
|
next $ Command.Add.cleanup file newkey True
|
||||||
|
where
|
||||||
|
cleantmp t = liftIO $ whenM (doesFileExist t) $ removeFile t
|
||||||
|
link src newkey = getViaTmpUnchecked newkey $ \t -> do
|
||||||
|
-- Make a hard link to the old backend's
|
||||||
|
-- cached key, to avoid wasting disk space.
|
||||||
|
liftIO $ unlessM (doesFileExist t) $ createLink src t
|
||||||
|
return True
|
141
Command/Move.hs
Normal file
141
Command/Move.hs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Move where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Command.Drop
|
||||||
|
import qualified Annex
|
||||||
|
import Annex.Content
|
||||||
|
import qualified Remote
|
||||||
|
import Annex.UUID
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck toOpt $ dontCheck fromOpt $
|
||||||
|
command "move" paramPaths seek
|
||||||
|
"move content of files to/from another repository"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesInGit $ whenAnnexed $ start True]
|
||||||
|
|
||||||
|
start :: Bool -> FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start move file (key, _) = do
|
||||||
|
noAuto
|
||||||
|
to <- Annex.getState Annex.toremote
|
||||||
|
from <- Annex.getState Annex.fromremote
|
||||||
|
case (from, to) of
|
||||||
|
(Nothing, Nothing) -> error "specify either --from or --to"
|
||||||
|
(Nothing, Just name) -> do
|
||||||
|
dest <- Remote.byName name
|
||||||
|
toStart dest move file key
|
||||||
|
(Just name, Nothing) -> do
|
||||||
|
src <- Remote.byName name
|
||||||
|
fromStart src move file key
|
||||||
|
(_ , _) -> error "only one of --from or --to can be specified"
|
||||||
|
where
|
||||||
|
noAuto = when move $ whenM (Annex.getState Annex.auto) $ error
|
||||||
|
"--auto is not supported for move"
|
||||||
|
|
||||||
|
showMoveAction :: Bool -> FilePath -> Annex ()
|
||||||
|
showMoveAction True file = showStart "move" file
|
||||||
|
showMoveAction False file = showStart "copy" file
|
||||||
|
|
||||||
|
{- Moves (or copies) the content of an annexed file to a remote.
|
||||||
|
-
|
||||||
|
- If the remote already has the content, it is still removed from
|
||||||
|
- the current repository.
|
||||||
|
-
|
||||||
|
- Note that unlike drop, this does not honor annex.numcopies.
|
||||||
|
- A file's content can be moved even if there are insufficient copies to
|
||||||
|
- allow it to be dropped.
|
||||||
|
-}
|
||||||
|
toStart :: Remote.Remote Annex -> Bool -> FilePath -> Key -> CommandStart
|
||||||
|
toStart dest move file key = do
|
||||||
|
u <- getUUID
|
||||||
|
ishere <- inAnnex key
|
||||||
|
if not ishere || u == Remote.uuid dest
|
||||||
|
then stop -- not here, so nothing to do
|
||||||
|
else do
|
||||||
|
showMoveAction move file
|
||||||
|
next $ toPerform dest move key
|
||||||
|
toPerform :: Remote.Remote Annex -> Bool -> Key -> CommandPerform
|
||||||
|
toPerform dest move key = moveLock move key $ do
|
||||||
|
-- Checking the remote is expensive, so not done in the start step.
|
||||||
|
-- In fast mode, location tracking is assumed to be correct,
|
||||||
|
-- and an explicit check is not done, when copying. When moving,
|
||||||
|
-- it has to be done, to avoid inaverdent data loss.
|
||||||
|
fast <- Annex.getState Annex.fast
|
||||||
|
let fastcheck = fast && not move && not (Remote.hasKeyCheap dest)
|
||||||
|
isthere <- if fastcheck
|
||||||
|
then do
|
||||||
|
remotes <- Remote.keyPossibilities key
|
||||||
|
return $ Right $ dest `elem` remotes
|
||||||
|
else Remote.hasKey dest key
|
||||||
|
case isthere of
|
||||||
|
Left err -> do
|
||||||
|
showNote err
|
||||||
|
stop
|
||||||
|
Right False -> do
|
||||||
|
showAction $ "to " ++ Remote.name dest
|
||||||
|
ok <- Remote.storeKey dest key
|
||||||
|
if ok
|
||||||
|
then finish
|
||||||
|
else do
|
||||||
|
when fastcheck $
|
||||||
|
warning "This could have failed because --fast is enabled."
|
||||||
|
stop
|
||||||
|
Right True -> finish
|
||||||
|
where
|
||||||
|
finish = do
|
||||||
|
Remote.logStatus dest key True
|
||||||
|
if move
|
||||||
|
then do
|
||||||
|
whenM (inAnnex key) $ removeAnnex key
|
||||||
|
next $ Command.Drop.cleanupLocal key
|
||||||
|
else next $ return True
|
||||||
|
|
||||||
|
{- Moves (or copies) the content of an annexed file from a remote
|
||||||
|
- to the current repository.
|
||||||
|
-
|
||||||
|
- If the current repository already has the content, it is still removed
|
||||||
|
- from the remote.
|
||||||
|
-}
|
||||||
|
fromStart :: Remote.Remote Annex -> Bool -> FilePath -> Key -> CommandStart
|
||||||
|
fromStart src move file key
|
||||||
|
| move = go
|
||||||
|
| otherwise = stopUnless (not <$> inAnnex key) go
|
||||||
|
where
|
||||||
|
go = stopUnless (fromOk src key) $ do
|
||||||
|
showMoveAction move file
|
||||||
|
next $ fromPerform src move key
|
||||||
|
fromOk :: Remote.Remote Annex -> Key -> Annex Bool
|
||||||
|
fromOk src key = do
|
||||||
|
u <- getUUID
|
||||||
|
remotes <- Remote.keyPossibilities key
|
||||||
|
return $ u /= Remote.uuid src && any (== src) remotes
|
||||||
|
fromPerform :: Remote.Remote Annex -> Bool -> Key -> CommandPerform
|
||||||
|
fromPerform src move key = moveLock move key $ do
|
||||||
|
ishere <- inAnnex key
|
||||||
|
if ishere
|
||||||
|
then handle move True
|
||||||
|
else do
|
||||||
|
showAction $ "from " ++ Remote.name src
|
||||||
|
ok <- getViaTmp key $ Remote.retrieveKeyFile src key
|
||||||
|
handle move ok
|
||||||
|
where
|
||||||
|
handle _ False = stop -- failed
|
||||||
|
handle False True = next $ return True -- copy complete
|
||||||
|
handle True True = do -- finish moving
|
||||||
|
ok <- Remote.removeKey src key
|
||||||
|
next $ Command.Drop.cleanupRemote key src ok
|
||||||
|
|
||||||
|
{- Locks a key in order for it to be moved.
|
||||||
|
- No lock is needed when a key is being copied. -}
|
||||||
|
moveLock :: Bool -> Key -> Annex a -> Annex a
|
||||||
|
moveLock True key a = lockContent key a
|
||||||
|
moveLock False _ a = a
|
33
Command/PreCommit.hs
Normal file
33
Command/PreCommit.hs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.PreCommit where
|
||||||
|
|
||||||
|
import Command
|
||||||
|
import qualified Command.Add
|
||||||
|
import qualified Command.Fix
|
||||||
|
import Backend
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "pre-commit" paramPaths seek "run by git pre-commit hook"]
|
||||||
|
|
||||||
|
{- The pre-commit hook needs to fix symlinks to all files being committed.
|
||||||
|
- And, it needs to inject unlocked files into the annex. -}
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek =
|
||||||
|
[ withFilesToBeCommitted $ whenAnnexed Command.Fix.start
|
||||||
|
, withFilesUnlockedToBeCommitted start]
|
||||||
|
|
||||||
|
start :: BackendFile -> CommandStart
|
||||||
|
start p = next $ perform p
|
||||||
|
|
||||||
|
perform :: BackendFile -> CommandPerform
|
||||||
|
perform pair@(_, file) = do
|
||||||
|
ok <- doCommand $ Command.Add.start pair
|
||||||
|
if ok
|
||||||
|
then next $ return True
|
||||||
|
else error $ "failed to add " ++ file ++ "; canceling commit"
|
34
Command/RecvKey.hs
Normal file
34
Command/RecvKey.hs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.RecvKey where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import CmdLine
|
||||||
|
import Annex.Content
|
||||||
|
import Utility.RsyncFile
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "recvkey" paramKey seek
|
||||||
|
"runs rsync in server mode to receive content"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withKeys start]
|
||||||
|
|
||||||
|
start :: Key -> CommandStart
|
||||||
|
start key = do
|
||||||
|
whenM (inAnnex key) $ error "key is already present in annex"
|
||||||
|
|
||||||
|
ok <- getViaTmp key (liftIO . rsyncServerReceive)
|
||||||
|
if ok
|
||||||
|
then do
|
||||||
|
-- forcibly quit after receiving one key,
|
||||||
|
-- and shutdown cleanly so queued git commands run
|
||||||
|
_ <- shutdown
|
||||||
|
liftIO exitSuccess
|
||||||
|
else liftIO exitFailure
|
56
Command/Reinject.hs
Normal file
56
Command/Reinject.hs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Reinject where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Logs.Location
|
||||||
|
import Annex.Content
|
||||||
|
import qualified Command.Fsck
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "reinject" (paramPair "SRC" "DEST") seek
|
||||||
|
"sets content of annexed file"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [FilePath] -> CommandStart
|
||||||
|
start (src:dest:[])
|
||||||
|
| src == dest = stop
|
||||||
|
| otherwise = do
|
||||||
|
ifAnnexed src
|
||||||
|
(error $ "cannot used annexed file as src: " ++ src)
|
||||||
|
go
|
||||||
|
where
|
||||||
|
go = do
|
||||||
|
showStart "reinject" dest
|
||||||
|
next $ whenAnnexed (perform src) dest
|
||||||
|
start _ = error "specify a src file and a dest file"
|
||||||
|
|
||||||
|
perform :: FilePath -> FilePath -> (Key, Backend Annex) -> CommandPerform
|
||||||
|
perform src _dest (key, backend) = do
|
||||||
|
unlessM move $ error "mv failed!"
|
||||||
|
next $ cleanup key backend
|
||||||
|
where
|
||||||
|
-- the file might be on a different filesystem,
|
||||||
|
-- so mv is used rather than simply calling
|
||||||
|
-- moveToObjectDir; disk space is also
|
||||||
|
-- checked this way.
|
||||||
|
move = getViaTmp key $ \tmp ->
|
||||||
|
liftIO $ boolSystem "mv" [File src, File tmp]
|
||||||
|
|
||||||
|
cleanup :: Key -> Backend Annex -> CommandCleanup
|
||||||
|
cleanup key backend = do
|
||||||
|
logStatus key InfoPresent
|
||||||
|
|
||||||
|
-- fsck the new content
|
||||||
|
size_ok <- Command.Fsck.checkKeySize key
|
||||||
|
backend_ok <- Command.Fsck.checkBackend backend key
|
||||||
|
|
||||||
|
return $ size_ok && backend_ok
|
32
Command/Semitrust.hs
Normal file
32
Command/Semitrust.hs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Semitrust where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import Logs.Trust
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "semitrust" (paramRepeating paramRemote) seek
|
||||||
|
"return repository to default trust level"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start ws = do
|
||||||
|
let name = unwords ws
|
||||||
|
showStart "semitrust" name
|
||||||
|
u <- Remote.nameToUUID name
|
||||||
|
next $ perform u
|
||||||
|
|
||||||
|
perform :: UUID -> CommandPerform
|
||||||
|
perform uuid = do
|
||||||
|
trustSet uuid SemiTrusted
|
||||||
|
next $ return True
|
28
Command/SendKey.hs
Normal file
28
Command/SendKey.hs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.SendKey where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Annex.Content
|
||||||
|
import Utility.RsyncFile
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "sendkey" paramKey seek
|
||||||
|
"runs rsync in server mode to send content"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withKeys start]
|
||||||
|
|
||||||
|
start :: Key -> CommandStart
|
||||||
|
start key = do
|
||||||
|
file <- inRepo $ gitAnnexLocation key
|
||||||
|
whenM (inAnnex key) $
|
||||||
|
liftIO $ rsyncServerSend file -- does not return
|
||||||
|
warning "requested key is not present"
|
||||||
|
liftIO exitFailure
|
198
Command/Status.hs
Normal file
198
Command/Status.hs
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Status where
|
||||||
|
|
||||||
|
import Control.Monad.State
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import qualified Data.Set as S
|
||||||
|
import Data.Set (Set)
|
||||||
|
import Text.JSON
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Types.Backend as B
|
||||||
|
import qualified Types.Remote as R
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Command.Unused
|
||||||
|
import qualified Git
|
||||||
|
import qualified Annex
|
||||||
|
import Command
|
||||||
|
import Utility.DataUnits
|
||||||
|
import Annex.Content
|
||||||
|
import Types.Key
|
||||||
|
import Backend
|
||||||
|
import Logs.UUID
|
||||||
|
import Logs.Trust
|
||||||
|
import Remote
|
||||||
|
|
||||||
|
-- a named computation that produces a statistic
|
||||||
|
type Stat = StatState (Maybe (String, StatState String))
|
||||||
|
|
||||||
|
-- cached info that multiple Stats may need
|
||||||
|
data StatInfo = StatInfo
|
||||||
|
{ keysPresentCache :: Maybe (Set Key)
|
||||||
|
, keysReferencedCache :: Maybe (Set Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- a state monad for running Stats in
|
||||||
|
type StatState = StateT StatInfo Annex
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "status" paramNothing seek
|
||||||
|
"shows status information about the annex"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNothing start]
|
||||||
|
|
||||||
|
{- Order is significant. Less expensive operations, and operations
|
||||||
|
- that share data go together.
|
||||||
|
-}
|
||||||
|
fast_stats :: [Stat]
|
||||||
|
fast_stats =
|
||||||
|
[ supported_backends
|
||||||
|
, supported_remote_types
|
||||||
|
, remote_list Trusted "trusted"
|
||||||
|
, remote_list SemiTrusted "semitrusted"
|
||||||
|
, remote_list UnTrusted "untrusted"
|
||||||
|
, remote_list DeadTrusted "dead"
|
||||||
|
]
|
||||||
|
slow_stats :: [Stat]
|
||||||
|
slow_stats =
|
||||||
|
[ tmp_size
|
||||||
|
, bad_data_size
|
||||||
|
, local_annex_keys
|
||||||
|
, local_annex_size
|
||||||
|
, visible_annex_keys
|
||||||
|
, visible_annex_size
|
||||||
|
, backend_usage
|
||||||
|
]
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
fast <- Annex.getState Annex.fast
|
||||||
|
let stats = if fast then fast_stats else fast_stats ++ slow_stats
|
||||||
|
showCustom "status" $ do
|
||||||
|
evalStateT (mapM_ showStat stats) (StatInfo Nothing Nothing)
|
||||||
|
return True
|
||||||
|
stop
|
||||||
|
|
||||||
|
stat :: String -> (String -> StatState String) -> Stat
|
||||||
|
stat desc a = return $ Just (desc, a desc)
|
||||||
|
|
||||||
|
nostat :: Stat
|
||||||
|
nostat = return Nothing
|
||||||
|
|
||||||
|
json :: JSON j => (j -> String) -> StatState j -> String -> StatState String
|
||||||
|
json serialize a desc = do
|
||||||
|
j <- a
|
||||||
|
lift $ maybeShowJSON [(desc, j)]
|
||||||
|
return $ serialize j
|
||||||
|
|
||||||
|
nojson :: StatState String -> String -> StatState String
|
||||||
|
nojson a _ = a
|
||||||
|
|
||||||
|
showStat :: Stat -> StatState ()
|
||||||
|
showStat s = calc =<< s
|
||||||
|
where
|
||||||
|
calc (Just (desc, a)) = do
|
||||||
|
(lift . showHeader) desc
|
||||||
|
lift . showRaw =<< a
|
||||||
|
calc Nothing = return ()
|
||||||
|
|
||||||
|
supported_backends :: Stat
|
||||||
|
supported_backends = stat "supported backends" $ json unwords $
|
||||||
|
return $ map B.name Backend.list
|
||||||
|
|
||||||
|
supported_remote_types :: Stat
|
||||||
|
supported_remote_types = stat "supported remote types" $ json unwords $
|
||||||
|
return $ map R.typename Remote.remoteTypes
|
||||||
|
|
||||||
|
remote_list :: TrustLevel -> String -> Stat
|
||||||
|
remote_list level desc = stat n $ nojson $ lift $ do
|
||||||
|
us <- M.keys <$> (M.union <$> uuidMap <*> remoteMap)
|
||||||
|
rs <- fst <$> trustPartition level us
|
||||||
|
s <- prettyPrintUUIDs n rs
|
||||||
|
return $ if null s then "0" else show (length rs) ++ "\n" ++ beginning s
|
||||||
|
where
|
||||||
|
n = desc ++ " repositories"
|
||||||
|
|
||||||
|
local_annex_size :: Stat
|
||||||
|
local_annex_size = stat "local annex size" $ json id $
|
||||||
|
keySizeSum <$> cachedKeysPresent
|
||||||
|
|
||||||
|
local_annex_keys :: Stat
|
||||||
|
local_annex_keys = stat "local annex keys" $ json show $
|
||||||
|
S.size <$> cachedKeysPresent
|
||||||
|
|
||||||
|
visible_annex_size :: Stat
|
||||||
|
visible_annex_size = stat "visible annex size" $ json id $
|
||||||
|
keySizeSum <$> cachedKeysReferenced
|
||||||
|
|
||||||
|
visible_annex_keys :: Stat
|
||||||
|
visible_annex_keys = stat "visible annex keys" $ json show $
|
||||||
|
S.size <$> cachedKeysReferenced
|
||||||
|
|
||||||
|
tmp_size :: Stat
|
||||||
|
tmp_size = staleSize "temporary directory size" gitAnnexTmpDir
|
||||||
|
|
||||||
|
bad_data_size :: Stat
|
||||||
|
bad_data_size = staleSize "bad keys size" gitAnnexBadDir
|
||||||
|
|
||||||
|
backend_usage :: Stat
|
||||||
|
backend_usage = stat "backend usage" $ nojson $
|
||||||
|
usage <$> cachedKeysReferenced <*> cachedKeysPresent
|
||||||
|
where
|
||||||
|
usage a b = pp "" $ reverse . sort $ map swap $ splits $ S.toList $ S.union a b
|
||||||
|
splits :: [Key] -> [(String, Integer)]
|
||||||
|
splits ks = M.toList $ M.fromListWith (+) $ map tcount ks
|
||||||
|
tcount k = (keyBackendName k, 1)
|
||||||
|
swap (a, b) = (b, a)
|
||||||
|
pp c [] = c
|
||||||
|
pp c ((n, b):xs) = "\n\t" ++ b ++ ": " ++ show n ++ pp c xs
|
||||||
|
|
||||||
|
cachedKeysPresent :: StatState (Set Key)
|
||||||
|
cachedKeysPresent = do
|
||||||
|
s <- get
|
||||||
|
case keysPresentCache s of
|
||||||
|
Just v -> return v
|
||||||
|
Nothing -> do
|
||||||
|
keys <- S.fromList <$> lift getKeysPresent
|
||||||
|
put s { keysPresentCache = Just keys }
|
||||||
|
return keys
|
||||||
|
|
||||||
|
cachedKeysReferenced :: StatState (Set Key)
|
||||||
|
cachedKeysReferenced = do
|
||||||
|
s <- get
|
||||||
|
case keysReferencedCache s of
|
||||||
|
Just v -> return v
|
||||||
|
Nothing -> do
|
||||||
|
keys <- S.fromList <$> lift Command.Unused.getKeysReferenced
|
||||||
|
put s { keysReferencedCache = Just keys }
|
||||||
|
return keys
|
||||||
|
|
||||||
|
keySizeSum :: Set Key -> String
|
||||||
|
keySizeSum s = total ++ missingnote
|
||||||
|
where
|
||||||
|
knownsizes = mapMaybe keySize $ S.toList s
|
||||||
|
total = roughSize storageUnits False $ sum knownsizes
|
||||||
|
missing = S.size s - genericLength knownsizes
|
||||||
|
missingnote
|
||||||
|
| missing == 0 = ""
|
||||||
|
| otherwise = aside $
|
||||||
|
"+ " ++ show missing ++
|
||||||
|
" keys of unknown size"
|
||||||
|
|
||||||
|
staleSize :: String -> (Git.Repo -> FilePath) -> Stat
|
||||||
|
staleSize label dirspec = do
|
||||||
|
keys <- lift (Command.Unused.staleKeys dirspec)
|
||||||
|
if null keys
|
||||||
|
then nostat
|
||||||
|
else stat label $ json (++ aside "clean up with git-annex unused") $
|
||||||
|
return $ keySizeSum $ S.fromList keys
|
||||||
|
|
||||||
|
aside :: String -> String
|
||||||
|
aside s = " (" ++ s ++ ")"
|
74
Command/Sync.hs
Normal file
74
Command/Sync.hs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Sync where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import qualified Git.Command
|
||||||
|
import qualified Git.Config
|
||||||
|
import qualified Git.Ref
|
||||||
|
import qualified Git
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "sync" paramPaths seek "synchronize local repository with remote"]
|
||||||
|
|
||||||
|
-- syncing involves several operations, any of which can independantly fail
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = map withNothing [commit, pull, push]
|
||||||
|
|
||||||
|
commit :: CommandStart
|
||||||
|
commit = do
|
||||||
|
showStart "commit" ""
|
||||||
|
next $ next $ do
|
||||||
|
showOutput
|
||||||
|
-- Commit will fail when the tree is clean, so ignore failure.
|
||||||
|
_ <- inRepo $ Git.Command.runBool "commit"
|
||||||
|
[Param "-a", Param "-m", Param "sync"]
|
||||||
|
return True
|
||||||
|
|
||||||
|
pull :: CommandStart
|
||||||
|
pull = do
|
||||||
|
remote <- defaultRemote
|
||||||
|
showStart "pull" remote
|
||||||
|
next $ next $ do
|
||||||
|
showOutput
|
||||||
|
checkRemote remote
|
||||||
|
inRepo $ Git.Command.runBool "pull" [Param remote]
|
||||||
|
|
||||||
|
push :: CommandStart
|
||||||
|
push = do
|
||||||
|
remote <- defaultRemote
|
||||||
|
showStart "push" remote
|
||||||
|
next $ next $ do
|
||||||
|
Annex.Branch.update
|
||||||
|
showOutput
|
||||||
|
inRepo $ Git.Command.runBool "push" [Param remote, matchingbranches]
|
||||||
|
where
|
||||||
|
-- git push may be configured to not push matching
|
||||||
|
-- branches; this should ensure it always does.
|
||||||
|
matchingbranches = Param ":"
|
||||||
|
|
||||||
|
-- the remote defaults to origin when not configured
|
||||||
|
defaultRemote :: Annex String
|
||||||
|
defaultRemote = do
|
||||||
|
branch <- currentBranch
|
||||||
|
fromRepo $ Git.Config.get ("branch." ++ branch ++ ".remote") "origin"
|
||||||
|
|
||||||
|
currentBranch :: Annex String
|
||||||
|
currentBranch = Git.Ref.describe . Git.Ref . firstLine . L.unpack <$>
|
||||||
|
inRepo (Git.Command.pipeRead [Param "symbolic-ref", Param "HEAD"])
|
||||||
|
|
||||||
|
checkRemote :: String -> Annex ()
|
||||||
|
checkRemote remote = do
|
||||||
|
remoteurl <- fromRepo $
|
||||||
|
Git.Config.get ("remote." ++ remote ++ ".url") ""
|
||||||
|
when (null remoteurl) $ do
|
||||||
|
error $ "No url is configured for the remote: " ++ remote
|
31
Command/Trust.hs
Normal file
31
Command/Trust.hs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Trust where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import Logs.Trust
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "trust" (paramRepeating paramRemote) seek "trust a repository"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start ws = do
|
||||||
|
let name = unwords ws
|
||||||
|
showStart "trust" name
|
||||||
|
u <- Remote.nameToUUID name
|
||||||
|
next $ perform u
|
||||||
|
|
||||||
|
perform :: UUID -> CommandPerform
|
||||||
|
perform uuid = do
|
||||||
|
trustSet uuid Trusted
|
||||||
|
next $ return True
|
34
Command/TweakFetch.hs
Normal file
34
Command/TweakFetch.hs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.TweakFetch where
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Command
|
||||||
|
import qualified Git.TweakFetch
|
||||||
|
import qualified Annex.Branch
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "tweak-fetch" paramNothing seek "run by git tweak-fetch hook"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [ withNothing start]
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
-- First, pass the hook's input through to its output, unchanged.
|
||||||
|
fetched <- liftIO $ Git.TweakFetch.runHook return
|
||||||
|
|
||||||
|
-- If one of the fetched refs is going to be stored on a git-annex
|
||||||
|
-- tracking branch, then merge in the new sha for that ref.
|
||||||
|
let tomerge = filter siblings fetched
|
||||||
|
unless (null tomerge) $ Annex.Branch.updateTo $ map topairs tomerge
|
||||||
|
stop
|
||||||
|
where
|
||||||
|
siblings f = suffix `isSuffixOf` (show $ Git.TweakFetch.local f)
|
||||||
|
suffix = "/" ++ show Annex.Branch.name
|
||||||
|
topairs f = (Git.TweakFetch.sha f, Git.TweakFetch.local f)
|
62
Command/Unannex.hs
Normal file
62
Command/Unannex.hs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Unannex where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Annex
|
||||||
|
import Utility.FileMode
|
||||||
|
import Logs.Location
|
||||||
|
import Annex.Content
|
||||||
|
import qualified Git.Command
|
||||||
|
import qualified Git.LsFiles as LsFiles
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "unannex" paramPaths seek "undo accidential add command"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesInGit $ whenAnnexed start]
|
||||||
|
|
||||||
|
start :: FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start file (key, _) = stopUnless (inAnnex key) $ do
|
||||||
|
showStart "unannex" file
|
||||||
|
next $ perform file key
|
||||||
|
|
||||||
|
perform :: FilePath -> Key -> CommandPerform
|
||||||
|
perform file key = next $ cleanup file key
|
||||||
|
|
||||||
|
cleanup :: FilePath -> Key -> CommandCleanup
|
||||||
|
cleanup file key = do
|
||||||
|
liftIO $ removeFile file
|
||||||
|
-- git rm deletes empty directory without --cached
|
||||||
|
inRepo $ Git.Command.run "rm" [Params "--cached --quiet --", File file]
|
||||||
|
|
||||||
|
-- If the file was already committed, it is now staged for removal.
|
||||||
|
-- Commit that removal now, to avoid later confusing the
|
||||||
|
-- pre-commit hook if this file is later added back to
|
||||||
|
-- git as a normal, non-annexed file.
|
||||||
|
whenM (not . null <$> inRepo (LsFiles.staged [file])) $ do
|
||||||
|
showOutput
|
||||||
|
inRepo $ Git.Command.run "commit" [
|
||||||
|
Param "-q",
|
||||||
|
Params "-m", Param "content removed from git annex",
|
||||||
|
Param "--", File file]
|
||||||
|
|
||||||
|
fast <- Annex.getState Annex.fast
|
||||||
|
if fast
|
||||||
|
then do
|
||||||
|
-- fast mode: hard link to content in annex
|
||||||
|
src <- inRepo $ gitAnnexLocation key
|
||||||
|
liftIO $ do
|
||||||
|
createLink src file
|
||||||
|
allowWrite file
|
||||||
|
else do
|
||||||
|
fromAnnex key file
|
||||||
|
logStatus key InfoMissing
|
||||||
|
|
||||||
|
return True
|
63
Command/Uninit.hs
Normal file
63
Command/Uninit.hs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Uninit where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as B
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Command
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Command.Unannex
|
||||||
|
import Init
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import Annex.Content
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [addCheck check $ command "uninit" paramPaths seek
|
||||||
|
"de-initialize git-annex and clean out repository"]
|
||||||
|
|
||||||
|
check :: Annex ()
|
||||||
|
check = do
|
||||||
|
b <- current_branch
|
||||||
|
when (b == Annex.Branch.name) $ error $
|
||||||
|
"cannot uninit when the " ++ show b ++ " branch is checked out"
|
||||||
|
where
|
||||||
|
current_branch = Git.Ref . Prelude.head . lines . B.unpack <$> revhead
|
||||||
|
revhead = inRepo $ Git.Command.pipeRead
|
||||||
|
[Params "rev-parse --abbrev-ref HEAD"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesInGit $ whenAnnexed startUnannex, withNothing start]
|
||||||
|
|
||||||
|
startUnannex :: FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
startUnannex file info = do
|
||||||
|
-- Force fast mode before running unannex. This way, if multiple
|
||||||
|
-- files link to a key, it will be left in the annex and hardlinked
|
||||||
|
-- to by each.
|
||||||
|
Annex.changeState $ \s -> s { Annex.fast = True }
|
||||||
|
Command.Unannex.start file info
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = next perform
|
||||||
|
|
||||||
|
perform :: CommandPerform
|
||||||
|
perform = next cleanup
|
||||||
|
|
||||||
|
cleanup :: CommandCleanup
|
||||||
|
cleanup = do
|
||||||
|
annexdir <- fromRepo gitAnnexDir
|
||||||
|
uninitialize
|
||||||
|
mapM_ removeAnnex =<< getKeysPresent
|
||||||
|
liftIO $ removeDirectoryRecursive annexdir
|
||||||
|
-- avoid normal shutdown
|
||||||
|
saveState
|
||||||
|
inRepo $ Git.Command.run "branch"
|
||||||
|
[Param "-D", Param $ show Annex.Branch.name]
|
||||||
|
liftIO exitSuccess
|
52
Command/Unlock.hs
Normal file
52
Command/Unlock.hs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Unlock where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Annex.Content
|
||||||
|
import Utility.CopyFile
|
||||||
|
import Utility.FileMode
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def =
|
||||||
|
[ c "unlock" "unlock files for modification"
|
||||||
|
, c "edit" "same as unlock"
|
||||||
|
]
|
||||||
|
where
|
||||||
|
c n = command n paramPaths seek
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesInGit $ whenAnnexed start]
|
||||||
|
|
||||||
|
{- The unlock subcommand replaces the symlink with a copy of the file's
|
||||||
|
- content. -}
|
||||||
|
start :: FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start file (key, _) = do
|
||||||
|
showStart "unlock" file
|
||||||
|
next $ perform file key
|
||||||
|
|
||||||
|
perform :: FilePath -> Key -> CommandPerform
|
||||||
|
perform dest key = do
|
||||||
|
unlessM (inAnnex key) $ error "content not present"
|
||||||
|
|
||||||
|
checkDiskSpace key
|
||||||
|
|
||||||
|
src <- inRepo $ gitAnnexLocation key
|
||||||
|
tmpdest <- fromRepo $ gitAnnexTmpLocation key
|
||||||
|
liftIO $ createDirectoryIfMissing True (parentDir tmpdest)
|
||||||
|
showAction "copying"
|
||||||
|
ok <- liftIO $ copyFileExternal src tmpdest
|
||||||
|
if ok
|
||||||
|
then do
|
||||||
|
liftIO $ do
|
||||||
|
removeFile dest
|
||||||
|
moveFile tmpdest dest
|
||||||
|
allowWrite dest
|
||||||
|
next $ return True
|
||||||
|
else error "copy failed!"
|
32
Command/Untrust.hs
Normal file
32
Command/Untrust.hs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Untrust where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Remote
|
||||||
|
import Logs.Trust
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "untrust" (paramRepeating paramRemote) seek
|
||||||
|
"do not trust a repository"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withWords start]
|
||||||
|
|
||||||
|
start :: [String] -> CommandStart
|
||||||
|
start ws = do
|
||||||
|
let name = unwords ws
|
||||||
|
showStart "untrust" name
|
||||||
|
u <- Remote.nameToUUID name
|
||||||
|
next $ perform u
|
||||||
|
|
||||||
|
perform :: UUID -> CommandPerform
|
||||||
|
perform uuid = do
|
||||||
|
trustSet uuid UnTrusted
|
||||||
|
next $ return True
|
235
Command/Unused.hs
Normal file
235
Command/Unused.hs
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE BangPatterns #-}
|
||||||
|
|
||||||
|
module Command.Unused where
|
||||||
|
|
||||||
|
import qualified Data.Set as S
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Annex.Content
|
||||||
|
import Utility.FileMode
|
||||||
|
import Utility.TempFile
|
||||||
|
import Logs.Location
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Command
|
||||||
|
import qualified Git.Ref
|
||||||
|
import qualified Git.LsFiles as LsFiles
|
||||||
|
import qualified Git.LsTree as LsTree
|
||||||
|
import qualified Backend
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import Annex.CatFile
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck fromOpt $ command "unused" paramNothing seek
|
||||||
|
"look for unused file content"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNothing start]
|
||||||
|
|
||||||
|
{- Finds unused content in the annex. -}
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
from <- Annex.getState Annex.fromremote
|
||||||
|
let (name, action) = case from of
|
||||||
|
Nothing -> (".", checkUnused)
|
||||||
|
Just "." -> (".", checkUnused)
|
||||||
|
Just n -> (n, checkRemoteUnused n)
|
||||||
|
showStart "unused" name
|
||||||
|
next action
|
||||||
|
|
||||||
|
checkUnused :: CommandPerform
|
||||||
|
checkUnused = do
|
||||||
|
(unused, stalebad, staletmp) <- unusedKeys
|
||||||
|
_ <- list "" unusedMsg unused 0 >>=
|
||||||
|
list "bad" staleBadMsg stalebad >>=
|
||||||
|
list "tmp" staleTmpMsg staletmp
|
||||||
|
next $ return True
|
||||||
|
where
|
||||||
|
list file msg l c = do
|
||||||
|
let unusedlist = number c l
|
||||||
|
unless (null l) $ showLongNote $ msg unusedlist
|
||||||
|
writeUnusedFile file unusedlist
|
||||||
|
return $ c + length l
|
||||||
|
|
||||||
|
checkRemoteUnused :: String -> CommandPerform
|
||||||
|
checkRemoteUnused name = do
|
||||||
|
checkRemoteUnused' =<< Remote.byName name
|
||||||
|
next $ return True
|
||||||
|
|
||||||
|
checkRemoteUnused' :: Remote.Remote Annex -> Annex ()
|
||||||
|
checkRemoteUnused' r = do
|
||||||
|
showAction "checking for unused data"
|
||||||
|
remotehas <- loggedKeysFor (Remote.uuid r)
|
||||||
|
remoteunused <- excludeReferenced remotehas
|
||||||
|
let list = number 0 remoteunused
|
||||||
|
writeUnusedFile "" list
|
||||||
|
unless (null remoteunused) $ showLongNote $ remoteUnusedMsg r list
|
||||||
|
|
||||||
|
writeUnusedFile :: FilePath -> [(Int, Key)] -> Annex ()
|
||||||
|
writeUnusedFile prefix l = do
|
||||||
|
logfile <- fromRepo $ gitAnnexUnusedLog prefix
|
||||||
|
liftIO $ viaTmp writeFile logfile $
|
||||||
|
unlines $ map (\(n, k) -> show n ++ " " ++ show k) l
|
||||||
|
|
||||||
|
table :: [(Int, Key)] -> [String]
|
||||||
|
table l = " NUMBER KEY" : map cols l
|
||||||
|
where
|
||||||
|
cols (n,k) = " " ++ pad 6 (show n) ++ " " ++ show k
|
||||||
|
pad n s = s ++ replicate (n - length s) ' '
|
||||||
|
|
||||||
|
number :: Int -> [a] -> [(Int, a)]
|
||||||
|
number _ [] = []
|
||||||
|
number n (x:xs) = (n+1, x) : number (n+1) xs
|
||||||
|
|
||||||
|
staleTmpMsg :: [(Int, Key)] -> String
|
||||||
|
staleTmpMsg t = unlines $
|
||||||
|
["Some partially transferred data exists in temporary files:"]
|
||||||
|
++ table t ++ [dropMsg Nothing]
|
||||||
|
|
||||||
|
staleBadMsg :: [(Int, Key)] -> String
|
||||||
|
staleBadMsg t = unlines $
|
||||||
|
["Some corrupted files have been preserved by fsck, just in case:"]
|
||||||
|
++ table t ++ [dropMsg Nothing]
|
||||||
|
|
||||||
|
unusedMsg :: [(Int, Key)] -> String
|
||||||
|
unusedMsg u = unusedMsg' u
|
||||||
|
["Some annexed data is no longer used by any files:"]
|
||||||
|
[dropMsg Nothing]
|
||||||
|
unusedMsg' :: [(Int, Key)] -> [String] -> [String] -> String
|
||||||
|
unusedMsg' u header trailer = unlines $
|
||||||
|
header ++
|
||||||
|
table u ++
|
||||||
|
["(To see where data was previously used, try: git log --stat -S'KEY')"] ++
|
||||||
|
trailer
|
||||||
|
|
||||||
|
remoteUnusedMsg :: Remote.Remote Annex -> [(Int, Key)] -> String
|
||||||
|
remoteUnusedMsg r u = unusedMsg' u
|
||||||
|
["Some annexed data on " ++ name ++ " is not used by any files:"]
|
||||||
|
[dropMsg $ Just r]
|
||||||
|
where
|
||||||
|
name = Remote.name r
|
||||||
|
|
||||||
|
dropMsg :: Maybe (Remote.Remote Annex) -> String
|
||||||
|
dropMsg Nothing = dropMsg' ""
|
||||||
|
dropMsg (Just r) = dropMsg' $ " --from " ++ Remote.name r
|
||||||
|
dropMsg' :: String -> String
|
||||||
|
dropMsg' s = "\nTo remove unwanted data: git-annex dropunused" ++ s ++ " NUMBER\n"
|
||||||
|
|
||||||
|
{- Finds keys whose content is present, but that do not seem to be used
|
||||||
|
- by any files in the git repo, or that are only present as bad or tmp
|
||||||
|
- files. -}
|
||||||
|
unusedKeys :: Annex ([Key], [Key], [Key])
|
||||||
|
unusedKeys = do
|
||||||
|
fast <- Annex.getState Annex.fast
|
||||||
|
if fast
|
||||||
|
then do
|
||||||
|
showNote "fast mode enabled; only finding stale files"
|
||||||
|
tmp <- staleKeys gitAnnexTmpDir
|
||||||
|
bad <- staleKeys gitAnnexBadDir
|
||||||
|
return ([], bad, tmp)
|
||||||
|
else do
|
||||||
|
showAction "checking for unused data"
|
||||||
|
present <- getKeysPresent
|
||||||
|
unused <- excludeReferenced present
|
||||||
|
staletmp <- staleKeysPrune gitAnnexTmpDir present
|
||||||
|
stalebad <- staleKeysPrune gitAnnexBadDir present
|
||||||
|
return (unused, stalebad, staletmp)
|
||||||
|
|
||||||
|
{- Finds keys in the list that are not referenced in the git repository. -}
|
||||||
|
excludeReferenced :: [Key] -> Annex [Key]
|
||||||
|
excludeReferenced [] = return [] -- optimisation
|
||||||
|
excludeReferenced l = do
|
||||||
|
c <- inRepo $ Git.Command.pipeRead [Param "show-ref"]
|
||||||
|
removewith (getKeysReferenced : map getKeysReferencedInGit (refs c))
|
||||||
|
(S.fromList l)
|
||||||
|
where
|
||||||
|
-- Skip the git-annex branches, and get all other unique refs.
|
||||||
|
refs = map (Git.Ref . snd) .
|
||||||
|
nubBy uniqref .
|
||||||
|
filter ourbranches .
|
||||||
|
map (separate (== ' ')) . lines . L.unpack
|
||||||
|
uniqref (a, _) (b, _) = a == b
|
||||||
|
ourbranchend = '/' : show Annex.Branch.name
|
||||||
|
ourbranches (_, b) = not $ ourbranchend `isSuffixOf` b
|
||||||
|
removewith [] s = return $ S.toList s
|
||||||
|
removewith (a:as) s
|
||||||
|
| s == S.empty = return [] -- optimisation
|
||||||
|
| otherwise = do
|
||||||
|
referenced <- a
|
||||||
|
let !s' = s `S.difference` S.fromList referenced
|
||||||
|
removewith as s'
|
||||||
|
|
||||||
|
{- Finds items in the first, smaller list, that are not
|
||||||
|
- present in the second, larger list.
|
||||||
|
-
|
||||||
|
- Constructing a single set, of the list that tends to be
|
||||||
|
- smaller, appears more efficient in both memory and CPU
|
||||||
|
- than constructing and taking the S.difference of two sets. -}
|
||||||
|
exclude :: Ord a => [a] -> [a] -> [a]
|
||||||
|
exclude [] _ = [] -- optimisation
|
||||||
|
exclude smaller larger = S.toList $ remove larger $ S.fromList smaller
|
||||||
|
where
|
||||||
|
remove a b = foldl (flip S.delete) b a
|
||||||
|
|
||||||
|
{- List of keys referenced by symlinks in the git repo. -}
|
||||||
|
getKeysReferenced :: Annex [Key]
|
||||||
|
getKeysReferenced = do
|
||||||
|
top <- fromRepo Git.workTree
|
||||||
|
files <- inRepo $ LsFiles.inRepo [top]
|
||||||
|
keypairs <- mapM Backend.lookupFile files
|
||||||
|
return $ map fst $ catMaybes keypairs
|
||||||
|
|
||||||
|
{- List of keys referenced by symlinks in a git ref. -}
|
||||||
|
getKeysReferencedInGit :: Git.Ref -> Annex [Key]
|
||||||
|
getKeysReferencedInGit ref = do
|
||||||
|
showAction $ "checking " ++ Git.Ref.describe ref
|
||||||
|
findkeys [] =<< inRepo (LsTree.lsTree ref)
|
||||||
|
where
|
||||||
|
findkeys c [] = return c
|
||||||
|
findkeys c (l:ls)
|
||||||
|
| isSymLink (LsTree.mode l) = do
|
||||||
|
content <- catFile ref $ LsTree.file l
|
||||||
|
case fileKey (takeFileName $ L.unpack content) of
|
||||||
|
Nothing -> findkeys c ls
|
||||||
|
Just k -> findkeys (k:c) ls
|
||||||
|
| otherwise = findkeys c ls
|
||||||
|
|
||||||
|
{- Looks in the specified directory for bad/tmp keys, and returns a list
|
||||||
|
- of those that might still have value, or might be stale and removable.
|
||||||
|
-
|
||||||
|
- When a list of presently available keys is provided, stale keys
|
||||||
|
- that no longer have value are deleted.
|
||||||
|
-}
|
||||||
|
staleKeysPrune :: (Git.Repo -> FilePath) -> [Key] -> Annex [Key]
|
||||||
|
staleKeysPrune dirspec present = do
|
||||||
|
contents <- staleKeys dirspec
|
||||||
|
|
||||||
|
let stale = contents `exclude` present
|
||||||
|
let dups = contents `exclude` stale
|
||||||
|
|
||||||
|
dir <- fromRepo dirspec
|
||||||
|
liftIO $ forM_ dups $ \t -> removeFile $ dir </> keyFile t
|
||||||
|
|
||||||
|
return stale
|
||||||
|
|
||||||
|
staleKeys :: (Git.Repo -> FilePath) -> Annex [Key]
|
||||||
|
staleKeys dirspec = do
|
||||||
|
dir <- fromRepo dirspec
|
||||||
|
exists <- liftIO $ doesDirectoryExist dir
|
||||||
|
if not exists
|
||||||
|
then return []
|
||||||
|
else do
|
||||||
|
contents <- liftIO $ getDirectoryContents dir
|
||||||
|
files <- liftIO $ filterM doesFileExist $
|
||||||
|
map (dir </>) contents
|
||||||
|
return $ mapMaybe (fileKey . takeFileName) files
|
27
Command/Upgrade.hs
Normal file
27
Command/Upgrade.hs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Upgrade where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import Upgrade
|
||||||
|
import Annex.Version
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [dontCheck repoExists $ -- because an old version may not seem to exist
|
||||||
|
command "upgrade" paramNothing seek "upgrade repository layout"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNothing start]
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
showStart "upgrade" "."
|
||||||
|
r <- upgrade
|
||||||
|
setVersion
|
||||||
|
next $ next $ return r
|
36
Command/Version.hs
Normal file
36
Command/Version.hs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Version where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Command
|
||||||
|
import qualified Build.SysConfig as SysConfig
|
||||||
|
import Annex.Version
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [noRepo showPackageVersion $ dontCheck repoExists $
|
||||||
|
command "version" paramNothing seek "show version info"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withNothing start]
|
||||||
|
|
||||||
|
start :: CommandStart
|
||||||
|
start = do
|
||||||
|
v <- getVersion
|
||||||
|
liftIO $ do
|
||||||
|
showPackageVersion
|
||||||
|
putStrLn $ "local repository version: " ++ fromMaybe "unknown" v
|
||||||
|
putStrLn $ "default repository version: " ++ defaultVersion
|
||||||
|
putStrLn $ "supported repository versions: " ++ vs supportedVersions
|
||||||
|
putStrLn $ "upgrade supported from repository versions: " ++ vs upgradableVersions
|
||||||
|
stop
|
||||||
|
where
|
||||||
|
vs = join " "
|
||||||
|
|
||||||
|
showPackageVersion :: IO ()
|
||||||
|
showPackageVersion = putStrLn $ "git-annex version: " ++ SysConfig.packageversion
|
41
Command/Whereis.hs
Normal file
41
Command/Whereis.hs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{- git-annex command
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Command.Whereis where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Logs.Location
|
||||||
|
import Command
|
||||||
|
import Remote
|
||||||
|
import Logs.Trust
|
||||||
|
|
||||||
|
def :: [Command]
|
||||||
|
def = [command "whereis" paramPaths seek
|
||||||
|
"lists repositories that have file content"]
|
||||||
|
|
||||||
|
seek :: [CommandSeek]
|
||||||
|
seek = [withFilesInGit $ whenAnnexed start]
|
||||||
|
|
||||||
|
start :: FilePath -> (Key, Backend Annex) -> CommandStart
|
||||||
|
start file (key, _) = do
|
||||||
|
showStart "whereis" file
|
||||||
|
next $ perform key
|
||||||
|
|
||||||
|
perform :: Key -> CommandPerform
|
||||||
|
perform key = do
|
||||||
|
(untrustedlocations, safelocations) <- trustPartition UnTrusted =<< keyLocations key
|
||||||
|
let num = length safelocations
|
||||||
|
showNote $ show num ++ " " ++ copiesplural num
|
||||||
|
pp <- prettyPrintUUIDs "whereis" safelocations
|
||||||
|
unless (null safelocations) $ showLongNote pp
|
||||||
|
pp' <- prettyPrintUUIDs "untrusted" untrustedlocations
|
||||||
|
unless (null untrustedlocations) $ showLongNote $ untrustedheader ++ pp'
|
||||||
|
if null safelocations then stop else next $ return True
|
||||||
|
where
|
||||||
|
copiesplural 1 = "copy"
|
||||||
|
copiesplural _ = "copies"
|
||||||
|
untrustedheader = "The following untrusted locations may also have copies:\n"
|
29
Common.hs
Normal file
29
Common.hs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
module Common (module X) where
|
||||||
|
|
||||||
|
import Control.Monad as X hiding (join)
|
||||||
|
import Control.Applicative as X
|
||||||
|
import Control.Monad.State as X (liftIO)
|
||||||
|
import Control.Exception.Extensible as X (IOException)
|
||||||
|
|
||||||
|
import Data.Maybe as X
|
||||||
|
import Data.List as X hiding (head, tail, init, last)
|
||||||
|
import Data.String.Utils as X
|
||||||
|
|
||||||
|
import System.Path as X
|
||||||
|
import System.FilePath as X
|
||||||
|
import System.Directory as X
|
||||||
|
import System.Cmd.Utils as X hiding (safeSystem)
|
||||||
|
import System.IO as X hiding (FilePath)
|
||||||
|
import System.Posix.Files as X
|
||||||
|
import System.Posix.IO as X
|
||||||
|
import System.Posix.Process as X hiding (executeFile)
|
||||||
|
import System.Exit as X
|
||||||
|
|
||||||
|
import Utility.Misc as X
|
||||||
|
import Utility.Conditional as X
|
||||||
|
import Utility.SafeCommand as X
|
||||||
|
import Utility.Path as X
|
||||||
|
import Utility.Directory as X
|
||||||
|
import Utility.Monad as X
|
||||||
|
|
||||||
|
import Utility.PartialPrelude as X
|
8
Common/Annex.hs
Normal file
8
Common/Annex.hs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module Common.Annex (module X) where
|
||||||
|
|
||||||
|
import Common as X
|
||||||
|
import Types as X
|
||||||
|
import Types.UUID as X (toUUID, fromUUID)
|
||||||
|
import Annex as X (gitRepo, inRepo, fromRepo)
|
||||||
|
import Locations as X
|
||||||
|
import Messages as X
|
85
Config.hs
Normal file
85
Config.hs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
{- Git configuration
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Config where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Git
|
||||||
|
import qualified Git.Config
|
||||||
|
import qualified Git.Command
|
||||||
|
import qualified Annex
|
||||||
|
|
||||||
|
type ConfigKey = String
|
||||||
|
|
||||||
|
{- Changes a git config setting in both internal state and .git/config -}
|
||||||
|
setConfig :: ConfigKey -> String -> Annex ()
|
||||||
|
setConfig k value = do
|
||||||
|
inRepo $ Git.Command.run "config" [Param k, Param value]
|
||||||
|
-- re-read git config and update the repo's state
|
||||||
|
newg <- inRepo Git.Config.read
|
||||||
|
Annex.changeState $ \s -> s { Annex.repo = newg }
|
||||||
|
|
||||||
|
{- Looks up a per-remote config setting in git config.
|
||||||
|
- Failing that, tries looking for a global config option. -}
|
||||||
|
getConfig :: Git.Repo -> ConfigKey -> String -> Annex String
|
||||||
|
getConfig r key def = do
|
||||||
|
def' <- fromRepo $ Git.Config.get ("annex." ++ key) def
|
||||||
|
fromRepo $ Git.Config.get (remoteConfig r key) def'
|
||||||
|
|
||||||
|
{- Looks up a per-remote config setting in git config. -}
|
||||||
|
remoteConfig :: Git.Repo -> ConfigKey -> String
|
||||||
|
remoteConfig r key = "remote." ++ fromMaybe "" (Git.remoteName r) ++ ".annex-" ++ key
|
||||||
|
|
||||||
|
{- Calculates cost for a remote. Either the default, or as configured
|
||||||
|
- by remote.<name>.annex-cost, or if remote.<name>.annex-cost-command
|
||||||
|
- is set and prints a number, that is used. -}
|
||||||
|
remoteCost :: Git.Repo -> Int -> Annex Int
|
||||||
|
remoteCost r def = do
|
||||||
|
cmd <- getConfig r "cost-command" ""
|
||||||
|
(fromMaybe def . readMaybe) <$>
|
||||||
|
if not $ null cmd
|
||||||
|
then liftIO $ snd <$> pipeFrom "sh" ["-c", cmd]
|
||||||
|
else getConfig r "cost" ""
|
||||||
|
|
||||||
|
cheapRemoteCost :: Int
|
||||||
|
cheapRemoteCost = 100
|
||||||
|
semiCheapRemoteCost :: Int
|
||||||
|
semiCheapRemoteCost = 110
|
||||||
|
expensiveRemoteCost :: Int
|
||||||
|
expensiveRemoteCost = 200
|
||||||
|
|
||||||
|
{- Adjusts a remote's cost to reflect it being encrypted. -}
|
||||||
|
encryptedRemoteCostAdj :: Int
|
||||||
|
encryptedRemoteCostAdj = 50
|
||||||
|
|
||||||
|
{- Make sure the remote cost numbers work out. -}
|
||||||
|
prop_cost_sane :: Bool
|
||||||
|
prop_cost_sane = False `notElem`
|
||||||
|
[ expensiveRemoteCost > 0
|
||||||
|
, cheapRemoteCost < semiCheapRemoteCost
|
||||||
|
, semiCheapRemoteCost < expensiveRemoteCost
|
||||||
|
, cheapRemoteCost + encryptedRemoteCostAdj > semiCheapRemoteCost
|
||||||
|
, cheapRemoteCost + encryptedRemoteCostAdj < expensiveRemoteCost
|
||||||
|
, semiCheapRemoteCost + encryptedRemoteCostAdj < expensiveRemoteCost
|
||||||
|
]
|
||||||
|
|
||||||
|
{- Checks if a repo should be ignored, based either on annex-ignore
|
||||||
|
- setting, or on command-line options. Allows command-line to override
|
||||||
|
- annex-ignore. -}
|
||||||
|
repoNotIgnored :: Git.Repo -> Annex Bool
|
||||||
|
repoNotIgnored r = not . Git.configTrue <$> getConfig r "ignore" "false"
|
||||||
|
|
||||||
|
{- If a value is specified, it is used; otherwise the default is looked up
|
||||||
|
- in git config. forcenumcopies overrides everything. -}
|
||||||
|
getNumCopies :: Maybe Int -> Annex Int
|
||||||
|
getNumCopies v = perhaps (use v) =<< Annex.getState Annex.forcenumcopies
|
||||||
|
where
|
||||||
|
use (Just n) = return n
|
||||||
|
use Nothing = perhaps (return 1) =<<
|
||||||
|
readMaybe <$> fromRepo (Git.Config.get config "1")
|
||||||
|
perhaps fallback = maybe fallback (return . id)
|
||||||
|
config = "annex.numcopies"
|
185
Crypto.hs
Normal file
185
Crypto.hs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
{- git-annex crypto
|
||||||
|
-
|
||||||
|
- Currently using gpg; could later be modified to support different
|
||||||
|
- crypto backends if neccessary.
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Crypto (
|
||||||
|
Cipher,
|
||||||
|
EncryptedCipher,
|
||||||
|
genCipher,
|
||||||
|
updateCipher,
|
||||||
|
describeCipher,
|
||||||
|
storeCipher,
|
||||||
|
extractCipher,
|
||||||
|
decryptCipher,
|
||||||
|
encryptKey,
|
||||||
|
withEncryptedHandle,
|
||||||
|
withDecryptedHandle,
|
||||||
|
withEncryptedContent,
|
||||||
|
withDecryptedContent,
|
||||||
|
|
||||||
|
prop_hmacWithCipher_sane
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import Data.ByteString.Lazy.UTF8 (fromString)
|
||||||
|
import Data.Digest.Pure.SHA
|
||||||
|
import Control.Applicative
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Utility.Gpg as Gpg
|
||||||
|
import Types.Key
|
||||||
|
import Types.Remote
|
||||||
|
import Utility.Base64
|
||||||
|
import Types.Crypto
|
||||||
|
|
||||||
|
{- The first half of a Cipher is used for HMAC; the remainder
|
||||||
|
- is used as the GPG symmetric encryption passphrase.
|
||||||
|
-
|
||||||
|
- HMAC SHA1 needs only 64 bytes. The remainder is for expansion,
|
||||||
|
- perhaps to HMAC SHA512, which needs 128 bytes (ideally).
|
||||||
|
-
|
||||||
|
- 256 is enough for gpg's symetric cipher; unlike weaker public key
|
||||||
|
- crypto, the key does not need to be too large.
|
||||||
|
-}
|
||||||
|
cipherHalf :: Int
|
||||||
|
cipherHalf = 256
|
||||||
|
|
||||||
|
cipherSize :: Int
|
||||||
|
cipherSize = cipherHalf * 2
|
||||||
|
|
||||||
|
cipherPassphrase :: Cipher -> String
|
||||||
|
cipherPassphrase (Cipher c) = drop cipherHalf c
|
||||||
|
|
||||||
|
cipherHmac :: Cipher -> String
|
||||||
|
cipherHmac (Cipher c) = take cipherHalf c
|
||||||
|
|
||||||
|
{- Creates a new Cipher, encrypted as specified in the remote's configuration -}
|
||||||
|
genCipher :: RemoteConfig -> IO EncryptedCipher
|
||||||
|
genCipher c = do
|
||||||
|
ks <- configKeyIds c
|
||||||
|
random <- genrandom
|
||||||
|
encryptCipher (Cipher random) ks
|
||||||
|
where
|
||||||
|
genrandom = Gpg.readStrict
|
||||||
|
-- Armor the random data, to avoid newlines,
|
||||||
|
-- since gpg only reads ciphers up to the first
|
||||||
|
-- newline.
|
||||||
|
[ Params "--gen-random --armor"
|
||||||
|
, Param $ show randomquality
|
||||||
|
, Param $ show cipherSize
|
||||||
|
]
|
||||||
|
-- 1 is /dev/urandom; 2 is /dev/random
|
||||||
|
randomquality = 1 :: Int
|
||||||
|
|
||||||
|
{- Updates an existing Cipher, re-encrypting it to add KeyIds specified in
|
||||||
|
- the remote's configuration. -}
|
||||||
|
updateCipher :: RemoteConfig -> EncryptedCipher -> IO EncryptedCipher
|
||||||
|
updateCipher c encipher@(EncryptedCipher _ ks) = do
|
||||||
|
ks' <- configKeyIds c
|
||||||
|
cipher <- decryptCipher c encipher
|
||||||
|
encryptCipher cipher (merge ks ks')
|
||||||
|
where
|
||||||
|
merge (KeyIds a) (KeyIds b) = KeyIds $ a ++ b
|
||||||
|
|
||||||
|
describeCipher :: EncryptedCipher -> String
|
||||||
|
describeCipher (EncryptedCipher _ (KeyIds ks)) =
|
||||||
|
"with gpg " ++ keys ks ++ " " ++ unwords ks
|
||||||
|
where
|
||||||
|
keys [_] = "key"
|
||||||
|
keys _ = "keys"
|
||||||
|
|
||||||
|
{- Stores an EncryptedCipher in a remote's configuration. -}
|
||||||
|
storeCipher :: RemoteConfig -> EncryptedCipher -> RemoteConfig
|
||||||
|
storeCipher c (EncryptedCipher t ks) =
|
||||||
|
M.insert "cipher" (toB64 t) $ M.insert "cipherkeys" (showkeys ks) c
|
||||||
|
where
|
||||||
|
showkeys (KeyIds l) = join "," l
|
||||||
|
|
||||||
|
{- Extracts an EncryptedCipher from a remote's configuration. -}
|
||||||
|
extractCipher :: RemoteConfig -> Maybe EncryptedCipher
|
||||||
|
extractCipher c =
|
||||||
|
case (M.lookup "cipher" c, M.lookup "cipherkeys" c) of
|
||||||
|
(Just t, Just ks) -> Just $ EncryptedCipher (fromB64 t) (readkeys ks)
|
||||||
|
_ -> Nothing
|
||||||
|
where
|
||||||
|
readkeys = KeyIds . split ","
|
||||||
|
|
||||||
|
{- Encrypts a Cipher to the specified KeyIds. -}
|
||||||
|
encryptCipher :: Cipher -> KeyIds -> IO EncryptedCipher
|
||||||
|
encryptCipher (Cipher c) (KeyIds ks) = do
|
||||||
|
let ks' = nub $ sort ks -- gpg complains about duplicate recipient keyids
|
||||||
|
encipher <- Gpg.pipeStrict (encrypt++recipients ks') c
|
||||||
|
return $ EncryptedCipher encipher (KeyIds ks')
|
||||||
|
where
|
||||||
|
encrypt = [ Params "--encrypt" ]
|
||||||
|
recipients l = force_recipients :
|
||||||
|
concatMap (\k -> [Param "--recipient", Param k]) l
|
||||||
|
-- Force gpg to only encrypt to the specified
|
||||||
|
-- recipients, not configured defaults.
|
||||||
|
force_recipients = Params "--no-encrypt-to --no-default-recipient"
|
||||||
|
|
||||||
|
{- Decrypting an EncryptedCipher is expensive; the Cipher should be cached. -}
|
||||||
|
decryptCipher :: RemoteConfig -> EncryptedCipher -> IO Cipher
|
||||||
|
decryptCipher _ (EncryptedCipher encipher _) =
|
||||||
|
Cipher <$> Gpg.pipeStrict decrypt encipher
|
||||||
|
where
|
||||||
|
decrypt = [ Param "--decrypt" ]
|
||||||
|
|
||||||
|
{- Generates an encrypted form of a Key. The encryption does not need to be
|
||||||
|
- reversable, nor does it need to be the same type of encryption used
|
||||||
|
- on content. It does need to be repeatable. -}
|
||||||
|
encryptKey :: Cipher -> Key -> Key
|
||||||
|
encryptKey c k = Key
|
||||||
|
{ keyName = hmacWithCipher c (show k)
|
||||||
|
, keyBackendName = "GPGHMACSHA1"
|
||||||
|
, keySize = Nothing -- size and mtime omitted
|
||||||
|
, keyMtime = Nothing -- to avoid leaking data
|
||||||
|
}
|
||||||
|
|
||||||
|
{- Runs an action, passing it a handle from which it can
|
||||||
|
- stream encrypted content. -}
|
||||||
|
withEncryptedHandle :: Cipher -> IO L.ByteString -> (Handle -> IO a) -> IO a
|
||||||
|
withEncryptedHandle = Gpg.passphraseHandle [Params "--symmetric --force-mdc"] . cipherPassphrase
|
||||||
|
|
||||||
|
{- Runs an action, passing it a handle from which it can
|
||||||
|
- stream decrypted content. -}
|
||||||
|
withDecryptedHandle :: Cipher -> IO L.ByteString -> (Handle -> IO a) -> IO a
|
||||||
|
withDecryptedHandle = Gpg.passphraseHandle [Param "--decrypt"] . cipherPassphrase
|
||||||
|
|
||||||
|
{- Streams encrypted content to an action. -}
|
||||||
|
withEncryptedContent :: Cipher -> IO L.ByteString -> (L.ByteString -> IO a) -> IO a
|
||||||
|
withEncryptedContent = pass withEncryptedHandle
|
||||||
|
|
||||||
|
{- Streams decrypted content to an action. -}
|
||||||
|
withDecryptedContent :: Cipher -> IO L.ByteString -> (L.ByteString -> IO a) -> IO a
|
||||||
|
withDecryptedContent = pass withDecryptedHandle
|
||||||
|
|
||||||
|
pass :: (Cipher -> IO L.ByteString -> (Handle -> IO a) -> IO a)
|
||||||
|
-> Cipher -> IO L.ByteString -> (L.ByteString -> IO a) -> IO a
|
||||||
|
pass to c i a = to c i $ \h -> a =<< L.hGetContents h
|
||||||
|
|
||||||
|
configKeyIds :: RemoteConfig -> IO KeyIds
|
||||||
|
configKeyIds c = Gpg.findPubKeys $ configGet c "encryption"
|
||||||
|
|
||||||
|
configGet :: RemoteConfig -> String -> String
|
||||||
|
configGet c key = fromMaybe missing $ M.lookup key c
|
||||||
|
where
|
||||||
|
missing = error $ "missing " ++ key ++ " in remote config"
|
||||||
|
|
||||||
|
hmacWithCipher :: Cipher -> String -> String
|
||||||
|
hmacWithCipher c = hmacWithCipher' (cipherHmac c)
|
||||||
|
hmacWithCipher' :: String -> String -> String
|
||||||
|
hmacWithCipher' c s = showDigest $ hmacSha1 (fromString c) (fromString s)
|
||||||
|
|
||||||
|
{- Ensure that hmacWithCipher' returns the same thing forevermore. -}
|
||||||
|
prop_hmacWithCipher_sane :: Bool
|
||||||
|
prop_hmacWithCipher_sane = known_good == hmacWithCipher' "foo" "bar"
|
||||||
|
where
|
||||||
|
known_good = "46b4ec586117154dacd49d664e5d63fdc88efb51"
|
1
GPL
Symbolic link
1
GPL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
doc/GPL
|
116
Git.hs
Normal file
116
Git.hs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
{- git repository handling
|
||||||
|
-
|
||||||
|
- This is written to be completely independant of git-annex and should be
|
||||||
|
- suitable for other uses.
|
||||||
|
-
|
||||||
|
- Copyright 2010, 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git (
|
||||||
|
Repo(..),
|
||||||
|
Ref(..),
|
||||||
|
Branch,
|
||||||
|
Sha,
|
||||||
|
Tag,
|
||||||
|
repoIsUrl,
|
||||||
|
repoIsSsh,
|
||||||
|
repoIsHttp,
|
||||||
|
repoIsLocalBare,
|
||||||
|
repoDescribe,
|
||||||
|
repoLocation,
|
||||||
|
workTree,
|
||||||
|
gitDir,
|
||||||
|
configTrue,
|
||||||
|
attributes,
|
||||||
|
assertLocal,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import Data.Char
|
||||||
|
import Network.URI (uriPath, uriScheme)
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git.Types
|
||||||
|
|
||||||
|
{- User-visible description of a git repo. -}
|
||||||
|
repoDescribe :: Repo -> String
|
||||||
|
repoDescribe Repo { remoteName = Just name } = name
|
||||||
|
repoDescribe Repo { location = Url url } = show url
|
||||||
|
repoDescribe Repo { location = Dir dir } = dir
|
||||||
|
repoDescribe Repo { location = Unknown } = "UNKNOWN"
|
||||||
|
|
||||||
|
{- Location of the repo, either as a path or url. -}
|
||||||
|
repoLocation :: Repo -> String
|
||||||
|
repoLocation Repo { location = Url url } = show url
|
||||||
|
repoLocation Repo { location = Dir dir } = dir
|
||||||
|
repoLocation Repo { location = Unknown } = undefined
|
||||||
|
|
||||||
|
{- Some code needs to vary between URL and normal repos,
|
||||||
|
- or bare and non-bare, these functions help with that. -}
|
||||||
|
repoIsUrl :: Repo -> Bool
|
||||||
|
repoIsUrl Repo { location = Url _ } = True
|
||||||
|
repoIsUrl _ = False
|
||||||
|
|
||||||
|
repoIsSsh :: Repo -> Bool
|
||||||
|
repoIsSsh Repo { location = Url url }
|
||||||
|
| scheme == "ssh:" = True
|
||||||
|
-- git treats these the same as ssh
|
||||||
|
| scheme == "git+ssh:" = True
|
||||||
|
| scheme == "ssh+git:" = True
|
||||||
|
| otherwise = False
|
||||||
|
where
|
||||||
|
scheme = uriScheme url
|
||||||
|
repoIsSsh _ = False
|
||||||
|
|
||||||
|
repoIsHttp :: Repo -> Bool
|
||||||
|
repoIsHttp Repo { location = Url url }
|
||||||
|
| uriScheme url == "http:" = True
|
||||||
|
| uriScheme url == "https:" = True
|
||||||
|
| otherwise = False
|
||||||
|
repoIsHttp _ = False
|
||||||
|
|
||||||
|
configAvail ::Repo -> Bool
|
||||||
|
configAvail Repo { config = c } = c /= M.empty
|
||||||
|
|
||||||
|
repoIsLocalBare :: Repo -> Bool
|
||||||
|
repoIsLocalBare r@(Repo { location = Dir _ }) = configAvail r && configBare r
|
||||||
|
repoIsLocalBare _ = False
|
||||||
|
|
||||||
|
assertLocal :: Repo -> a -> a
|
||||||
|
assertLocal repo action =
|
||||||
|
if not $ repoIsUrl repo
|
||||||
|
then action
|
||||||
|
else error $ "acting on non-local git repo " ++ repoDescribe repo ++
|
||||||
|
" not supported"
|
||||||
|
configBare :: Repo -> Bool
|
||||||
|
configBare repo = maybe unknown configTrue $ M.lookup "core.bare" $ config repo
|
||||||
|
where
|
||||||
|
unknown = error $ "it is not known if git repo " ++
|
||||||
|
repoDescribe repo ++
|
||||||
|
" is a bare repository; config not read"
|
||||||
|
|
||||||
|
{- Path to a repository's gitattributes file. -}
|
||||||
|
attributes :: Repo -> String
|
||||||
|
attributes repo
|
||||||
|
| configBare repo = workTree repo ++ "/info/.gitattributes"
|
||||||
|
| otherwise = workTree repo ++ "/.gitattributes"
|
||||||
|
|
||||||
|
{- Path to a repository's .git directory. -}
|
||||||
|
gitDir :: Repo -> String
|
||||||
|
gitDir repo
|
||||||
|
| configBare repo = workTree repo
|
||||||
|
| otherwise = workTree repo </> ".git"
|
||||||
|
|
||||||
|
{- Path to a repository's --work-tree, that is, its top.
|
||||||
|
-
|
||||||
|
- Note that for URL repositories, this is the path on the remote host. -}
|
||||||
|
workTree :: Repo -> FilePath
|
||||||
|
workTree Repo { location = Url u } = uriPath u
|
||||||
|
workTree Repo { location = Dir d } = d
|
||||||
|
workTree Repo { location = Unknown } = undefined
|
||||||
|
|
||||||
|
{- Checks if a string from git config is a true value. -}
|
||||||
|
configTrue :: String -> Bool
|
||||||
|
configTrue s = map toLower s == "true"
|
79
Git/Branch.hs
Normal file
79
Git/Branch.hs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
{- git branch stuff
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Branch where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Sha
|
||||||
|
import Git.Command
|
||||||
|
|
||||||
|
{- Checks if the second branch has any commits not present on the first
|
||||||
|
- branch. -}
|
||||||
|
changed :: Branch -> Branch -> Repo -> IO Bool
|
||||||
|
changed origbranch newbranch repo
|
||||||
|
| origbranch == newbranch = return False
|
||||||
|
| otherwise = not . L.null <$> diffs
|
||||||
|
where
|
||||||
|
diffs = pipeRead
|
||||||
|
[ Param "log"
|
||||||
|
, Param (show origbranch ++ ".." ++ show newbranch)
|
||||||
|
, Params "--oneline -n1"
|
||||||
|
] repo
|
||||||
|
|
||||||
|
{- Given a set of refs that are all known to have commits not
|
||||||
|
- on the branch, tries to update the branch by a fast-forward.
|
||||||
|
-
|
||||||
|
- In order for that to be possible, one of the refs must contain
|
||||||
|
- every commit present in all the other refs.
|
||||||
|
-}
|
||||||
|
fastForward :: Branch -> [Ref] -> Repo -> IO Bool
|
||||||
|
fastForward _ [] _ = return True
|
||||||
|
fastForward branch (first:rest) repo = do
|
||||||
|
-- First, check that the branch does not contain any
|
||||||
|
-- new commits that are not in the first ref. If it does,
|
||||||
|
-- cannot fast-forward.
|
||||||
|
diverged <- changed first branch repo
|
||||||
|
if diverged
|
||||||
|
then no_ff
|
||||||
|
else maybe no_ff do_ff =<< findbest first rest
|
||||||
|
where
|
||||||
|
no_ff = return False
|
||||||
|
do_ff to = do
|
||||||
|
run "update-ref"
|
||||||
|
[Param $ show branch, Param $ show to] repo
|
||||||
|
return True
|
||||||
|
findbest c [] = return $ Just c
|
||||||
|
findbest c (r:rs)
|
||||||
|
| c == r = findbest c rs
|
||||||
|
| otherwise = do
|
||||||
|
better <- changed c r repo
|
||||||
|
worse <- changed r c repo
|
||||||
|
case (better, worse) of
|
||||||
|
(True, True) -> return Nothing -- divergent fail
|
||||||
|
(True, False) -> findbest r rs -- better
|
||||||
|
(False, True) -> findbest c rs -- worse
|
||||||
|
(False, False) -> findbest c rs -- same
|
||||||
|
|
||||||
|
{- Commits the index into the specified branch (or other ref),
|
||||||
|
- with the specified parent refs, and returns the committed sha -}
|
||||||
|
commit :: String -> Branch -> [Ref] -> Repo -> IO Sha
|
||||||
|
commit message branch parentrefs repo = do
|
||||||
|
tree <- getSha "write-tree" $ asString $
|
||||||
|
pipeRead [Param "write-tree"] repo
|
||||||
|
sha <- getSha "commit-tree" $ asString $
|
||||||
|
ignorehandle $ pipeWriteRead
|
||||||
|
(map Param $ ["commit-tree", show tree] ++ ps)
|
||||||
|
(L.pack message) repo
|
||||||
|
run "update-ref" [Param $ show branch, Param $ show sha] repo
|
||||||
|
return sha
|
||||||
|
where
|
||||||
|
ignorehandle a = snd <$> a
|
||||||
|
asString a = L.unpack <$> a
|
||||||
|
ps = concatMap (\r -> ["-p", show r]) parentrefs
|
75
Git/CatFile.hs
Normal file
75
Git/CatFile.hs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
{- git cat-file interface
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.CatFile (
|
||||||
|
CatFileHandle,
|
||||||
|
catFileStart,
|
||||||
|
catFileStop,
|
||||||
|
catFile,
|
||||||
|
catObject
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad.State
|
||||||
|
import System.Cmd.Utils
|
||||||
|
import System.IO
|
||||||
|
import qualified Data.ByteString.Char8 as S
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Sha
|
||||||
|
import Git.Command
|
||||||
|
|
||||||
|
type CatFileHandle = (PipeHandle, Handle, Handle)
|
||||||
|
|
||||||
|
{- Starts git cat-file running in batch mode in a repo and returns a handle. -}
|
||||||
|
catFileStart :: Repo -> IO CatFileHandle
|
||||||
|
catFileStart repo = hPipeBoth "git" $ toCommand $
|
||||||
|
gitCommandLine [Param "cat-file", Param "--batch"] repo
|
||||||
|
|
||||||
|
{- Stops git cat-file. -}
|
||||||
|
catFileStop :: CatFileHandle -> IO ()
|
||||||
|
catFileStop (pid, from, to) = do
|
||||||
|
hClose to
|
||||||
|
hClose from
|
||||||
|
forceSuccess pid
|
||||||
|
|
||||||
|
{- Reads a file from a specified branch. -}
|
||||||
|
catFile :: CatFileHandle -> Branch -> FilePath -> IO L.ByteString
|
||||||
|
catFile h branch file = catObject h $ Ref $ show branch ++ ":" ++ file
|
||||||
|
|
||||||
|
{- Uses a running git cat-file read the content of an object.
|
||||||
|
- Objects that do not exist will have "" returned. -}
|
||||||
|
catObject :: CatFileHandle -> Ref -> IO L.ByteString
|
||||||
|
catObject (_, from, to) object = do
|
||||||
|
hPutStrLn to $ show object
|
||||||
|
hFlush to
|
||||||
|
header <- hGetLine from
|
||||||
|
case words header of
|
||||||
|
[sha, objtype, size]
|
||||||
|
| length sha == shaSize &&
|
||||||
|
validobjtype objtype -> handle size
|
||||||
|
| otherwise -> dne
|
||||||
|
_
|
||||||
|
| header == show object ++ " missing" -> dne
|
||||||
|
| otherwise -> error $ "unknown response from git cat-file " ++ header
|
||||||
|
where
|
||||||
|
handle size = case reads size of
|
||||||
|
[(bytes, "")] -> readcontent bytes
|
||||||
|
_ -> dne
|
||||||
|
readcontent bytes = do
|
||||||
|
content <- S.hGet from bytes
|
||||||
|
c <- hGetChar from
|
||||||
|
when (c /= '\n') $
|
||||||
|
error "missing newline from git cat-file"
|
||||||
|
return $ L.fromChunks [content]
|
||||||
|
dne = return L.empty
|
||||||
|
validobjtype t
|
||||||
|
| t == "blob" = True
|
||||||
|
| t == "commit" = True
|
||||||
|
| t == "tree" = True
|
||||||
|
| otherwise = False
|
66
Git/CheckAttr.hs
Normal file
66
Git/CheckAttr.hs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
{- git check-attr interface
|
||||||
|
-
|
||||||
|
- Copyright 2010, 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.CheckAttr where
|
||||||
|
|
||||||
|
import System.Exit
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Command
|
||||||
|
import qualified Git.Filename
|
||||||
|
import qualified Git.Version
|
||||||
|
|
||||||
|
{- Efficiently looks up a gitattributes value for each file in a list. -}
|
||||||
|
lookup :: String -> [FilePath] -> Repo -> IO [(FilePath, String)]
|
||||||
|
lookup attr files repo = do
|
||||||
|
cwd <- getCurrentDirectory
|
||||||
|
(_, fromh, toh) <- hPipeBoth "git" (toCommand params)
|
||||||
|
_ <- forkProcess $ do
|
||||||
|
hClose fromh
|
||||||
|
hPutStr toh $ join "\0" $ input cwd
|
||||||
|
hClose toh
|
||||||
|
exitSuccess
|
||||||
|
hClose toh
|
||||||
|
output cwd . lines <$> hGetContents fromh
|
||||||
|
where
|
||||||
|
params = gitCommandLine
|
||||||
|
[ Param "check-attr"
|
||||||
|
, Param attr
|
||||||
|
, Params "-z --stdin"
|
||||||
|
] repo
|
||||||
|
|
||||||
|
{- Before git 1.7.7, git check-attr worked best with
|
||||||
|
- absolute filenames; using them worked around some bugs
|
||||||
|
- with relative filenames.
|
||||||
|
-
|
||||||
|
- With newer git, git check-attr chokes on some absolute
|
||||||
|
- filenames, and the bugs that necessitated them were fixed,
|
||||||
|
- so use relative filenames. -}
|
||||||
|
oldgit = Git.Version.older "1.7.7"
|
||||||
|
input cwd
|
||||||
|
| oldgit = map (absPathFrom cwd) files
|
||||||
|
| otherwise = map (relPathDirToFile cwd . absPathFrom cwd) files
|
||||||
|
output cwd
|
||||||
|
| oldgit = map (torel cwd . topair)
|
||||||
|
| otherwise = map topair
|
||||||
|
|
||||||
|
topair l = (Git.Filename.decode file, value)
|
||||||
|
where
|
||||||
|
file = join sep $ beginning bits
|
||||||
|
value = end bits !! 0
|
||||||
|
bits = split sep l
|
||||||
|
sep = ": " ++ attr ++ ": "
|
||||||
|
|
||||||
|
torel cwd (file, value) = (relfile, value)
|
||||||
|
where
|
||||||
|
relfile
|
||||||
|
| startswith cwd' file = drop (length cwd') file
|
||||||
|
| otherwise = relPathDirToFile top' file
|
||||||
|
top = workTree repo
|
||||||
|
cwd' = cwd ++ "/"
|
||||||
|
top' = top ++ "/"
|
82
Git/Command.hs
Normal file
82
Git/Command.hs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
{- running git commands
|
||||||
|
-
|
||||||
|
- Copyright 2010, 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Command where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Types
|
||||||
|
|
||||||
|
{- Constructs a git command line operating on the specified repo. -}
|
||||||
|
gitCommandLine :: [CommandParam] -> Repo -> [CommandParam]
|
||||||
|
gitCommandLine params repo@(Repo { location = Dir _ } ) =
|
||||||
|
-- force use of specified repo via --git-dir and --work-tree
|
||||||
|
[ Param ("--git-dir=" ++ gitDir repo)
|
||||||
|
, Param ("--work-tree=" ++ workTree repo)
|
||||||
|
] ++ params
|
||||||
|
gitCommandLine _ repo = assertLocal repo $ error "internal"
|
||||||
|
|
||||||
|
{- Runs git in the specified repo. -}
|
||||||
|
runBool :: String -> [CommandParam] -> Repo -> IO Bool
|
||||||
|
runBool subcommand params repo = assertLocal repo $
|
||||||
|
boolSystem "git" $ gitCommandLine (Param subcommand : params) repo
|
||||||
|
|
||||||
|
{- Runs git in the specified repo, throwing an error if it fails. -}
|
||||||
|
run :: String -> [CommandParam] -> Repo -> IO ()
|
||||||
|
run subcommand params repo = assertLocal repo $
|
||||||
|
runBool subcommand params repo
|
||||||
|
>>! error $ "git " ++ show params ++ " failed"
|
||||||
|
|
||||||
|
{- Runs a git subcommand and returns its output, lazily.
|
||||||
|
-
|
||||||
|
- Note that this leaves the git process running, and so zombies will
|
||||||
|
- result unless reap is called.
|
||||||
|
-}
|
||||||
|
pipeRead :: [CommandParam] -> Repo -> IO L.ByteString
|
||||||
|
pipeRead params repo = assertLocal repo $ do
|
||||||
|
(_, h) <- hPipeFrom "git" $ toCommand $ gitCommandLine params repo
|
||||||
|
hSetBinaryMode h True
|
||||||
|
L.hGetContents h
|
||||||
|
|
||||||
|
{- Runs a git subcommand, feeding it input.
|
||||||
|
- You should call either getProcessStatus or forceSuccess on the PipeHandle. -}
|
||||||
|
pipeWrite :: [CommandParam] -> L.ByteString -> Repo -> IO PipeHandle
|
||||||
|
pipeWrite params s repo = assertLocal repo $ do
|
||||||
|
(p, h) <- hPipeTo "git" (toCommand $ gitCommandLine params repo)
|
||||||
|
L.hPut h s
|
||||||
|
hClose h
|
||||||
|
return p
|
||||||
|
|
||||||
|
{- Runs a git subcommand, feeding it input, and returning its output.
|
||||||
|
- You should call either getProcessStatus or forceSuccess on the PipeHandle. -}
|
||||||
|
pipeWriteRead :: [CommandParam] -> L.ByteString -> Repo -> IO (PipeHandle, L.ByteString)
|
||||||
|
pipeWriteRead params s repo = assertLocal repo $ do
|
||||||
|
(p, from, to) <- hPipeBoth "git" (toCommand $ gitCommandLine params repo)
|
||||||
|
hSetBinaryMode from True
|
||||||
|
L.hPut to s
|
||||||
|
hClose to
|
||||||
|
c <- L.hGetContents from
|
||||||
|
return (p, c)
|
||||||
|
|
||||||
|
{- Reads null terminated output of a git command (as enabled by the -z
|
||||||
|
- parameter), and splits it. -}
|
||||||
|
pipeNullSplit :: [CommandParam] -> Repo -> IO [String]
|
||||||
|
pipeNullSplit params repo = map L.unpack <$> pipeNullSplitB params repo
|
||||||
|
|
||||||
|
{- For when Strings are not needed. -}
|
||||||
|
pipeNullSplitB ::[CommandParam] -> Repo -> IO [L.ByteString]
|
||||||
|
pipeNullSplitB params repo = filter (not . L.null) . L.split '\0' <$>
|
||||||
|
pipeRead params repo
|
||||||
|
|
||||||
|
{- Reaps any zombie git processes. -}
|
||||||
|
reap :: IO ()
|
||||||
|
reap = do
|
||||||
|
-- throws an exception when there are no child processes
|
||||||
|
r <- catchDefaultIO (getAnyProcessStatus False True) Nothing
|
||||||
|
maybe (return ()) (const reap) r
|
60
Git/Config.hs
Normal file
60
Git/Config.hs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
{- git repository configuration handling
|
||||||
|
-
|
||||||
|
- Copyright 2010,2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Config where
|
||||||
|
|
||||||
|
import System.Posix.Directory
|
||||||
|
import Control.Exception (bracket_)
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Types
|
||||||
|
import qualified Git.Construct
|
||||||
|
|
||||||
|
{- Returns a single git config setting, or a default value if not set. -}
|
||||||
|
get :: String -> String -> Repo -> String
|
||||||
|
get key defaultValue repo = M.findWithDefault defaultValue key (config repo)
|
||||||
|
|
||||||
|
{- Runs git config and populates a repo with its config. -}
|
||||||
|
read :: Repo -> IO Repo
|
||||||
|
read repo@(Repo { location = Dir d }) = do
|
||||||
|
{- Cannot use pipeRead because it relies on the config having
|
||||||
|
been already read. Instead, chdir to the repo. -}
|
||||||
|
cwd <- getCurrentDirectory
|
||||||
|
bracket_ (changeWorkingDirectory d) (changeWorkingDirectory cwd) $
|
||||||
|
pOpen ReadFromPipe "git" ["config", "--null", "--list"] $
|
||||||
|
hRead repo
|
||||||
|
read r = assertLocal r $ error "internal"
|
||||||
|
|
||||||
|
{- Reads git config from a handle and populates a repo with it. -}
|
||||||
|
hRead :: Repo -> Handle -> IO Repo
|
||||||
|
hRead repo h = do
|
||||||
|
val <- hGetContentsStrict h
|
||||||
|
store val repo
|
||||||
|
|
||||||
|
{- Stores a git config into a repo, returning the new version of the repo.
|
||||||
|
- The git config may be multiple lines, or a single line. Config settings
|
||||||
|
- can be updated inrementally. -}
|
||||||
|
store :: String -> Repo -> IO Repo
|
||||||
|
store s repo = do
|
||||||
|
let repo' = repo { config = parse s `M.union` config repo }
|
||||||
|
rs <- Git.Construct.fromRemotes repo'
|
||||||
|
return $ repo' { remotes = rs }
|
||||||
|
|
||||||
|
{- Parses git config --list or git config --null --list output into a
|
||||||
|
- config map. -}
|
||||||
|
parse :: String -> M.Map String String
|
||||||
|
parse [] = M.empty
|
||||||
|
parse s
|
||||||
|
-- --list output will have an = in the first line
|
||||||
|
| all ('=' `elem`) (take 1 ls) = sep '=' ls
|
||||||
|
-- --null --list output separates keys from values with newlines
|
||||||
|
| otherwise = sep '\n' $ split "\0" s
|
||||||
|
where
|
||||||
|
ls = lines s
|
||||||
|
sep c = M.fromList . map (separate (== c))
|
215
Git/Construct.hs
Normal file
215
Git/Construct.hs
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
{- Construction of Git Repo objects
|
||||||
|
-
|
||||||
|
- Copyright 2010,2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Construct (
|
||||||
|
fromCwd,
|
||||||
|
fromAbsPath,
|
||||||
|
fromUrl,
|
||||||
|
fromUnknown,
|
||||||
|
localToUrl,
|
||||||
|
remoteNamed,
|
||||||
|
remoteNamedFromKey,
|
||||||
|
fromRemotes,
|
||||||
|
fromRemoteLocation,
|
||||||
|
repoAbsPath,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import System.Posix.User
|
||||||
|
import qualified Data.Map as M hiding (map, split)
|
||||||
|
import Network.URI
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git.Types
|
||||||
|
import Git
|
||||||
|
import qualified Git.Url as Url
|
||||||
|
|
||||||
|
{- Finds the current git repository, which may be in a parent directory. -}
|
||||||
|
fromCwd :: IO Repo
|
||||||
|
fromCwd = getCurrentDirectory >>= seekUp isRepoTop >>= maybe norepo makerepo
|
||||||
|
where
|
||||||
|
makerepo = return . newFrom . Dir
|
||||||
|
norepo = error "Not in a git repository."
|
||||||
|
|
||||||
|
{- Local Repo constructor, requires an absolute path to the repo be
|
||||||
|
- specified. -}
|
||||||
|
fromAbsPath :: FilePath -> IO Repo
|
||||||
|
fromAbsPath dir
|
||||||
|
| "/" `isPrefixOf` dir = do
|
||||||
|
-- Git always looks for "dir.git" in preference to
|
||||||
|
-- to "dir", even if dir ends in a "/".
|
||||||
|
let canondir = dropTrailingPathSeparator dir
|
||||||
|
let dir' = canondir ++ ".git"
|
||||||
|
e <- doesDirectoryExist dir'
|
||||||
|
if e
|
||||||
|
then ret dir'
|
||||||
|
else if "/.git" `isSuffixOf` canondir
|
||||||
|
then do
|
||||||
|
-- When dir == "foo/.git", git looks
|
||||||
|
-- for "foo/.git/.git", and failing
|
||||||
|
-- that, uses "foo" as the repository.
|
||||||
|
e' <- doesDirectoryExist $ dir </> ".git"
|
||||||
|
if e'
|
||||||
|
then ret dir
|
||||||
|
else ret $ takeDirectory canondir
|
||||||
|
else ret dir
|
||||||
|
| otherwise = error $ "internal error, " ++ dir ++ " is not absolute"
|
||||||
|
where
|
||||||
|
ret = return . newFrom . Dir
|
||||||
|
|
||||||
|
{- Remote Repo constructor. Throws exception on invalid url. -}
|
||||||
|
fromUrl :: String -> IO Repo
|
||||||
|
fromUrl url
|
||||||
|
| startswith "file://" url = fromAbsPath $ uriPath u
|
||||||
|
| otherwise = return $ newFrom $ Url u
|
||||||
|
where
|
||||||
|
u = fromMaybe bad $ parseURI url
|
||||||
|
bad = error $ "bad url " ++ url
|
||||||
|
|
||||||
|
{- Creates a repo that has an unknown location. -}
|
||||||
|
fromUnknown :: IO Repo
|
||||||
|
fromUnknown = return $ newFrom Unknown
|
||||||
|
|
||||||
|
{- Converts a local Repo into a remote repo, using the reference repo
|
||||||
|
- which is assumed to be on the same host. -}
|
||||||
|
localToUrl :: Repo -> Repo -> Repo
|
||||||
|
localToUrl reference r
|
||||||
|
| not $ repoIsUrl reference = error "internal error; reference repo not url"
|
||||||
|
| repoIsUrl r = r
|
||||||
|
| otherwise = r { location = Url $ fromJust $ parseURI absurl }
|
||||||
|
where
|
||||||
|
absurl =
|
||||||
|
Url.scheme reference ++ "//" ++
|
||||||
|
Url.authority reference ++
|
||||||
|
workTree r
|
||||||
|
|
||||||
|
{- Calculates a list of a repo's configured remotes, by parsing its config. -}
|
||||||
|
fromRemotes :: Repo -> IO [Repo]
|
||||||
|
fromRemotes repo = mapM construct remotepairs
|
||||||
|
where
|
||||||
|
filterconfig f = filter f $ M.toList $ config repo
|
||||||
|
filterkeys f = filterconfig (\(k,_) -> f k)
|
||||||
|
remotepairs = filterkeys isremote
|
||||||
|
isremote k = startswith "remote." k && endswith ".url" k
|
||||||
|
construct (k,v) = remoteNamedFromKey k $ fromRemoteLocation v repo
|
||||||
|
|
||||||
|
{- Sets the name of a remote when constructing the Repo to represent it. -}
|
||||||
|
remoteNamed :: String -> IO Repo -> IO Repo
|
||||||
|
remoteNamed n constructor = do
|
||||||
|
r <- constructor
|
||||||
|
return $ r { remoteName = Just n }
|
||||||
|
|
||||||
|
{- Sets the name of a remote based on the git config key, such as
|
||||||
|
"remote.foo.url". -}
|
||||||
|
remoteNamedFromKey :: String -> IO Repo -> IO Repo
|
||||||
|
remoteNamedFromKey k = remoteNamed basename
|
||||||
|
where
|
||||||
|
basename = join "." $ reverse $ drop 1 $
|
||||||
|
reverse $ drop 1 $ split "." k
|
||||||
|
|
||||||
|
{- Constructs a new Repo for one of a Repo's remotes using a given
|
||||||
|
- location (ie, an url). -}
|
||||||
|
fromRemoteLocation :: String -> Repo -> IO Repo
|
||||||
|
fromRemoteLocation s repo = gen $ calcloc s
|
||||||
|
where
|
||||||
|
filterconfig f = filter f $ M.toList $ config repo
|
||||||
|
gen v
|
||||||
|
| scpstyle v = fromUrl $ scptourl v
|
||||||
|
| isURI v = fromUrl v
|
||||||
|
| otherwise = fromRemotePath v repo
|
||||||
|
-- insteadof config can rewrite remote location
|
||||||
|
calcloc l
|
||||||
|
| null insteadofs = l
|
||||||
|
| otherwise = replacement ++ drop (length bestvalue) l
|
||||||
|
where
|
||||||
|
replacement = drop (length prefix) $
|
||||||
|
take (length bestkey - length suffix) bestkey
|
||||||
|
(bestkey, bestvalue) = maximumBy longestvalue insteadofs
|
||||||
|
longestvalue (_, a) (_, b) = compare b a
|
||||||
|
insteadofs = filterconfig $ \(k, v) ->
|
||||||
|
startswith prefix k &&
|
||||||
|
endswith suffix k &&
|
||||||
|
startswith v l
|
||||||
|
(prefix, suffix) = ("url." , ".insteadof")
|
||||||
|
-- git remotes can be written scp style -- [user@]host:dir
|
||||||
|
scpstyle v = ":" `isInfixOf` v && not ("//" `isInfixOf` v)
|
||||||
|
scptourl v = "ssh://" ++ host ++ slash dir
|
||||||
|
where
|
||||||
|
(host, dir) = separate (== ':') v
|
||||||
|
slash d | d == "" = "/~/" ++ d
|
||||||
|
| "/" `isPrefixOf` d = d
|
||||||
|
| "~" `isPrefixOf` d = '/':d
|
||||||
|
| otherwise = "/~/" ++ d
|
||||||
|
|
||||||
|
{- Constructs a Repo from the path specified in the git remotes of
|
||||||
|
- another Repo. -}
|
||||||
|
fromRemotePath :: FilePath -> Repo -> IO Repo
|
||||||
|
fromRemotePath dir repo = do
|
||||||
|
dir' <- expandTilde dir
|
||||||
|
fromAbsPath $ workTree repo </> dir'
|
||||||
|
|
||||||
|
{- Git remotes can have a directory that is specified relative
|
||||||
|
- to the user's home directory, or that contains tilde expansions.
|
||||||
|
- This converts such a directory to an absolute path.
|
||||||
|
- Note that it has to run on the system where the remote is.
|
||||||
|
-}
|
||||||
|
repoAbsPath :: FilePath -> IO FilePath
|
||||||
|
repoAbsPath d = do
|
||||||
|
d' <- expandTilde d
|
||||||
|
h <- myHomeDir
|
||||||
|
return $ h </> d'
|
||||||
|
|
||||||
|
expandTilde :: FilePath -> IO FilePath
|
||||||
|
expandTilde = expandt True
|
||||||
|
where
|
||||||
|
expandt _ [] = return ""
|
||||||
|
expandt _ ('/':cs) = do
|
||||||
|
v <- expandt True cs
|
||||||
|
return ('/':v)
|
||||||
|
expandt True ('~':'/':cs) = do
|
||||||
|
h <- myHomeDir
|
||||||
|
return $ h </> cs
|
||||||
|
expandt True ('~':cs) = do
|
||||||
|
let (name, rest) = findname "" cs
|
||||||
|
u <- getUserEntryForName name
|
||||||
|
return $ homeDirectory u </> rest
|
||||||
|
expandt _ (c:cs) = do
|
||||||
|
v <- expandt False cs
|
||||||
|
return (c:v)
|
||||||
|
findname n [] = (n, "")
|
||||||
|
findname n (c:cs)
|
||||||
|
| c == '/' = (n, cs)
|
||||||
|
| otherwise = findname (n++[c]) cs
|
||||||
|
|
||||||
|
seekUp :: (FilePath -> IO Bool) -> FilePath -> IO (Maybe FilePath)
|
||||||
|
seekUp want dir = do
|
||||||
|
ok <- want dir
|
||||||
|
if ok
|
||||||
|
then return $ Just dir
|
||||||
|
else case parentDir dir of
|
||||||
|
"" -> return Nothing
|
||||||
|
d -> seekUp want d
|
||||||
|
|
||||||
|
isRepoTop :: FilePath -> IO Bool
|
||||||
|
isRepoTop dir = do
|
||||||
|
r <- isRepo
|
||||||
|
b <- isBareRepo
|
||||||
|
return (r || b)
|
||||||
|
where
|
||||||
|
isRepo = gitSignature ".git" ".git/config"
|
||||||
|
isBareRepo = gitSignature "objects" "config"
|
||||||
|
gitSignature subdir file = liftM2 (&&)
|
||||||
|
(doesDirectoryExist (dir ++ "/" ++ subdir))
|
||||||
|
(doesFileExist (dir ++ "/" ++ file))
|
||||||
|
|
||||||
|
newFrom :: RepoLocation -> Repo
|
||||||
|
newFrom l =
|
||||||
|
Repo {
|
||||||
|
location = l,
|
||||||
|
config = M.empty,
|
||||||
|
remotes = [],
|
||||||
|
remoteName = Nothing
|
||||||
|
}
|
28
Git/Filename.hs
Normal file
28
Git/Filename.hs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{- Some git commands output encoded filenames, in a rather annoyingly complex
|
||||||
|
- C-style encoding.
|
||||||
|
-
|
||||||
|
- Copyright 2010, 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Filename where
|
||||||
|
|
||||||
|
import Utility.Format (decode_c, encode_c)
|
||||||
|
|
||||||
|
import Common
|
||||||
|
|
||||||
|
decode :: String -> FilePath
|
||||||
|
decode [] = []
|
||||||
|
decode f@(c:s)
|
||||||
|
-- encoded strings will be inside double quotes
|
||||||
|
| c == '"' && end s == ['"'] = decode_c $ beginning s
|
||||||
|
| otherwise = f
|
||||||
|
|
||||||
|
{- Should not need to use this, except for testing decode. -}
|
||||||
|
encode :: FilePath -> String
|
||||||
|
encode s = "\"" ++ encode_c s ++ "\""
|
||||||
|
|
||||||
|
{- for quickcheck -}
|
||||||
|
prop_idempotent_deencode :: String -> Bool
|
||||||
|
prop_idempotent_deencode s = s == decode (encode s)
|
32
Git/HashObject.hs
Normal file
32
Git/HashObject.hs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{- git hash-object interface
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.HashObject where
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Command
|
||||||
|
|
||||||
|
{- Injects a set of files into git, returning the shas of the objects
|
||||||
|
- and an IO action to call ones the the shas have been used. -}
|
||||||
|
hashFiles :: [FilePath] -> Repo -> IO ([Sha], IO ())
|
||||||
|
hashFiles paths repo = do
|
||||||
|
(pid, fromh, toh) <- hPipeBoth "git" $ toCommand $ git_hash_object repo
|
||||||
|
_ <- forkProcess (feeder toh)
|
||||||
|
hClose toh
|
||||||
|
shas <- map Ref . lines <$> hGetContentsStrict fromh
|
||||||
|
return (shas, ender fromh pid)
|
||||||
|
where
|
||||||
|
git_hash_object = gitCommandLine
|
||||||
|
[Param "hash-object", Param "-w", Param "--stdin-paths"]
|
||||||
|
feeder toh = do
|
||||||
|
hPutStr toh $ unlines paths
|
||||||
|
hClose toh
|
||||||
|
exitSuccess
|
||||||
|
ender fromh pid = do
|
||||||
|
hClose fromh
|
||||||
|
forceSuccess pid
|
24
Git/Index.hs
Normal file
24
Git/Index.hs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{- git index file stuff
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Index where
|
||||||
|
|
||||||
|
import System.Posix.Env (setEnv, unsetEnv, getEnv)
|
||||||
|
|
||||||
|
{- Forces git to use the specified index file.
|
||||||
|
-
|
||||||
|
- Returns an action that will reset back to the default
|
||||||
|
- index file. -}
|
||||||
|
override :: FilePath -> IO (IO ())
|
||||||
|
override index = do
|
||||||
|
res <- getEnv var
|
||||||
|
setEnv var index True
|
||||||
|
return $ reset res
|
||||||
|
where
|
||||||
|
var = "GIT_INDEX_FILE"
|
||||||
|
reset (Just v) = setEnv var v True
|
||||||
|
reset _ = unsetEnv var
|
71
Git/LsFiles.hs
Normal file
71
Git/LsFiles.hs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
{- git ls-files interface
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.LsFiles (
|
||||||
|
inRepo,
|
||||||
|
notInRepo,
|
||||||
|
staged,
|
||||||
|
stagedNotDeleted,
|
||||||
|
changedUnstaged,
|
||||||
|
typeChanged,
|
||||||
|
typeChangedStaged,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Command
|
||||||
|
|
||||||
|
{- Scans for files that are checked into git at the specified locations. -}
|
||||||
|
inRepo :: [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
inRepo l = pipeNullSplit $ Params "ls-files --cached -z --" : map File l
|
||||||
|
|
||||||
|
{- Scans for files at the specified locations that are not checked into git. -}
|
||||||
|
notInRepo :: Bool -> [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
notInRepo include_ignored l repo = pipeNullSplit params repo
|
||||||
|
where
|
||||||
|
params = [Params "ls-files --others"] ++ exclude ++
|
||||||
|
[Params "-z --"] ++ map File l
|
||||||
|
exclude
|
||||||
|
| include_ignored = []
|
||||||
|
| otherwise = [Param "--exclude-standard"]
|
||||||
|
|
||||||
|
{- Returns a list of all files that are staged for commit. -}
|
||||||
|
staged :: [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
staged = staged' []
|
||||||
|
|
||||||
|
{- Returns a list of the files, staged for commit, that are being added,
|
||||||
|
- moved, or changed (but not deleted), from the specified locations. -}
|
||||||
|
stagedNotDeleted :: [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
stagedNotDeleted = staged' [Param "--diff-filter=ACMRT"]
|
||||||
|
|
||||||
|
staged' :: [CommandParam] -> [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
staged' ps l = pipeNullSplit $ prefix ++ ps ++ suffix
|
||||||
|
where
|
||||||
|
prefix = [Params "diff --cached --name-only -z"]
|
||||||
|
suffix = Param "--" : map File l
|
||||||
|
|
||||||
|
{- Returns a list of files that have unstaged changes. -}
|
||||||
|
changedUnstaged :: [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
changedUnstaged l = pipeNullSplit params
|
||||||
|
where
|
||||||
|
params = Params "diff --name-only -z --" : map File l
|
||||||
|
|
||||||
|
{- Returns a list of the files in the specified locations that are staged
|
||||||
|
- for commit, and whose type has changed. -}
|
||||||
|
typeChangedStaged :: [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
typeChangedStaged = typeChanged' [Param "--cached"]
|
||||||
|
|
||||||
|
{- Returns a list of the files in the specified locations whose type has
|
||||||
|
- changed. Files only staged for commit will not be included. -}
|
||||||
|
typeChanged :: [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
typeChanged = typeChanged' []
|
||||||
|
|
||||||
|
typeChanged' :: [CommandParam] -> [FilePath] -> Repo -> IO [FilePath]
|
||||||
|
typeChanged' ps l = pipeNullSplit $ prefix ++ ps ++ suffix
|
||||||
|
where
|
||||||
|
prefix = [Params "diff --name-only --diff-filter=T -z"]
|
||||||
|
suffix = Param "--" : map File l
|
52
Git/LsTree.hs
Normal file
52
Git/LsTree.hs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{- git ls-tree interface
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.LsTree (
|
||||||
|
TreeItem(..),
|
||||||
|
lsTree,
|
||||||
|
parseLsTree
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Numeric
|
||||||
|
import Control.Applicative
|
||||||
|
import System.Posix.Types
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Command
|
||||||
|
import qualified Git.Filename
|
||||||
|
|
||||||
|
data TreeItem = TreeItem
|
||||||
|
{ mode :: FileMode
|
||||||
|
, typeobj :: String
|
||||||
|
, sha :: String
|
||||||
|
, file :: FilePath
|
||||||
|
} deriving Show
|
||||||
|
|
||||||
|
{- Lists the contents of a Ref -}
|
||||||
|
lsTree :: Ref -> Repo -> IO [TreeItem]
|
||||||
|
lsTree t repo = map parseLsTree <$>
|
||||||
|
pipeNullSplitB [Params "ls-tree --full-tree -z -r --", File $ show t] repo
|
||||||
|
|
||||||
|
{- Parses a line of ls-tree output.
|
||||||
|
- (The --long format is not currently supported.) -}
|
||||||
|
parseLsTree :: L.ByteString -> TreeItem
|
||||||
|
parseLsTree l = TreeItem
|
||||||
|
{ mode = fst $ Prelude.head $ readOct $ L.unpack m
|
||||||
|
, typeobj = L.unpack t
|
||||||
|
, sha = L.unpack s
|
||||||
|
, file = Git.Filename.decode $ L.unpack f
|
||||||
|
}
|
||||||
|
where
|
||||||
|
-- l = <mode> SP <type> SP <sha> TAB <file>
|
||||||
|
-- All fields are fixed, so we can pull them out of
|
||||||
|
-- specific positions in the line.
|
||||||
|
(m, past_m) = L.splitAt 7 l
|
||||||
|
(t, past_t) = L.splitAt 4 past_m
|
||||||
|
(s, past_s) = L.splitAt 40 $ L.tail past_t
|
||||||
|
f = L.tail past_s
|
93
Git/Queue.hs
Normal file
93
Git/Queue.hs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
{- git repository command queue
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Queue (
|
||||||
|
Queue,
|
||||||
|
new,
|
||||||
|
add,
|
||||||
|
size,
|
||||||
|
full,
|
||||||
|
flush
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import System.IO
|
||||||
|
import System.Cmd.Utils
|
||||||
|
import Data.String.Utils
|
||||||
|
import Utility.SafeCommand
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Command
|
||||||
|
|
||||||
|
{- An action to perform in a git repository. The file to act on
|
||||||
|
- is not included, and must be able to be appended after the params. -}
|
||||||
|
data Action = Action
|
||||||
|
{ getSubcommand :: String
|
||||||
|
, getParams :: [CommandParam]
|
||||||
|
} deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
{- A queue of actions to perform (in any order) on a git repository,
|
||||||
|
- with lists of files to perform them on. This allows coalescing
|
||||||
|
- similar git commands. -}
|
||||||
|
data Queue = Queue Int (M.Map Action [FilePath])
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
{- A recommended maximum size for the queue, after which it should be
|
||||||
|
- run.
|
||||||
|
-
|
||||||
|
- 10240 is semi-arbitrary. If we assume git filenames are between 10 and
|
||||||
|
- 255 characters long, then the queue will build up between 100kb and
|
||||||
|
- 2550kb long commands. The max command line length on linux is somewhere
|
||||||
|
- above 20k, so this is a fairly good balance -- the queue will buffer
|
||||||
|
- only a few megabytes of stuff and a minimal number of commands will be
|
||||||
|
- run by xargs. -}
|
||||||
|
maxSize :: Int
|
||||||
|
maxSize = 10240
|
||||||
|
|
||||||
|
{- Constructor for empty queue. -}
|
||||||
|
new :: Queue
|
||||||
|
new = Queue 0 M.empty
|
||||||
|
|
||||||
|
{- Adds an action to a queue. -}
|
||||||
|
add :: Queue -> String -> [CommandParam] -> [FilePath] -> Queue
|
||||||
|
add (Queue n m) subcommand params files = Queue (n + 1) m'
|
||||||
|
where
|
||||||
|
action = Action subcommand params
|
||||||
|
-- There are probably few items in the map, but there
|
||||||
|
-- can be a lot of files per item. So, optimise adding
|
||||||
|
-- files.
|
||||||
|
m' = M.insertWith' const action fs m
|
||||||
|
fs = files ++ M.findWithDefault [] action m
|
||||||
|
|
||||||
|
{- Number of items in a queue. -}
|
||||||
|
size :: Queue -> Int
|
||||||
|
size (Queue n _) = n
|
||||||
|
|
||||||
|
{- Is a queue large enough that it should be flushed? -}
|
||||||
|
full :: Queue -> Bool
|
||||||
|
full (Queue n _) = n > maxSize
|
||||||
|
|
||||||
|
{- Runs a queue on a git repository. -}
|
||||||
|
flush :: Queue -> Repo -> IO Queue
|
||||||
|
flush (Queue _ m) repo = do
|
||||||
|
forM_ (M.toList m) $ uncurry $ runAction repo
|
||||||
|
return new
|
||||||
|
|
||||||
|
{- Runs an Action on a list of files in a git repository.
|
||||||
|
-
|
||||||
|
- Complicated by commandline length limits.
|
||||||
|
-
|
||||||
|
- Intentionally runs the command even if the list of files is empty;
|
||||||
|
- this allows queueing commands that do not need a list of files. -}
|
||||||
|
runAction :: Repo -> Action -> [FilePath] -> IO ()
|
||||||
|
runAction repo action files =
|
||||||
|
pOpen WriteToPipe "xargs" ("-0":"git":params) feedxargs
|
||||||
|
where
|
||||||
|
params = toCommand $ gitCommandLine
|
||||||
|
(Param (getSubcommand action):getParams action) repo
|
||||||
|
feedxargs h = hPutStr h $ join "\0" files
|
48
Git/Ref.hs
Normal file
48
Git/Ref.hs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{- git ref stuff
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Ref where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Command
|
||||||
|
|
||||||
|
{- Converts a fully qualified git ref into a user-visible version. -}
|
||||||
|
describe :: Ref -> String
|
||||||
|
describe = remove "refs/heads/" . remove "refs/remotes/" . show
|
||||||
|
where
|
||||||
|
remove prefix s
|
||||||
|
| prefix `isPrefixOf` s = drop (length prefix) s
|
||||||
|
| otherwise = s
|
||||||
|
|
||||||
|
{- Checks if a ref exists. -}
|
||||||
|
exists :: Ref -> Repo -> IO Bool
|
||||||
|
exists ref = runBool "show-ref"
|
||||||
|
[Param "--verify", Param "-q", Param $ show ref]
|
||||||
|
|
||||||
|
{- Get the sha of a fully qualified git ref, if it exists. -}
|
||||||
|
sha :: Branch -> Repo -> IO (Maybe Sha)
|
||||||
|
sha branch repo = process . L.unpack <$> showref repo
|
||||||
|
where
|
||||||
|
showref = pipeRead [Param "show-ref",
|
||||||
|
Param "--hash", -- get the hash
|
||||||
|
Param $ show branch]
|
||||||
|
process [] = Nothing
|
||||||
|
process s = Just $ Ref $ firstLine s
|
||||||
|
|
||||||
|
{- List of (refs, branches) matching a given ref spec.
|
||||||
|
- Duplicate refs are filtered out. -}
|
||||||
|
matching :: Ref -> Repo -> IO [(Ref, Branch)]
|
||||||
|
matching ref repo = do
|
||||||
|
r <- pipeRead [Param "show-ref", Param $ show ref] repo
|
||||||
|
return $ nubBy uniqref $ map (gen . L.unpack) (L.lines r)
|
||||||
|
where
|
||||||
|
uniqref (a, _) (b, _) = a == b
|
||||||
|
gen l = let (r, b) = separate (== ' ') l in
|
||||||
|
(Ref r, Ref b)
|
39
Git/Sha.hs
Normal file
39
Git/Sha.hs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{- git SHA stuff
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Sha where
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git.Types
|
||||||
|
|
||||||
|
{- Runs an action that causes a git subcommand to emit a Sha, and strips
|
||||||
|
any trailing newline, returning the sha. -}
|
||||||
|
getSha :: String -> IO String -> IO Sha
|
||||||
|
getSha subcommand a = maybe bad return =<< extractSha <$> a
|
||||||
|
where
|
||||||
|
bad = error $ "failed to read sha from git " ++ subcommand
|
||||||
|
|
||||||
|
{- Extracts the Sha from a string. There can be a trailing newline after
|
||||||
|
- it, but nothing else. -}
|
||||||
|
extractSha :: String -> Maybe Sha
|
||||||
|
extractSha s
|
||||||
|
| len == shaSize = val s
|
||||||
|
| len == shaSize + 1 && length s' == shaSize = val s'
|
||||||
|
| otherwise = Nothing
|
||||||
|
where
|
||||||
|
len = length s
|
||||||
|
s' = firstLine s
|
||||||
|
val v
|
||||||
|
| isSha v = Just $ Ref v
|
||||||
|
| otherwise = Nothing
|
||||||
|
|
||||||
|
isSha :: String -> Bool
|
||||||
|
isSha v = all (`elem` "1234567890ABCDEFabcdef") v && length v == shaSize
|
||||||
|
|
||||||
|
{- Size of a git sha. -}
|
||||||
|
shaSize :: Int
|
||||||
|
shaSize = 40
|
79
Git/TweakFetch.hs
Normal file
79
Git/TweakFetch.hs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
{- git tweak-fetch hook support
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.TweakFetch (runHook, FetchedRef(..)) where
|
||||||
|
|
||||||
|
import Data.Either (rights)
|
||||||
|
import System.Posix.IO
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Sha
|
||||||
|
|
||||||
|
data FetchedRef = FetchedRef
|
||||||
|
{ sha :: Sha
|
||||||
|
, merge :: Bool
|
||||||
|
, remote :: Ref
|
||||||
|
, local :: Ref
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
{- Each line fed to the tweak-fetch hook should represent a ref that is
|
||||||
|
- being updated. It's important that the hook always outputs every line
|
||||||
|
- that is fed into it (possibly modified), otherwise incoming refs will
|
||||||
|
- not be stored. So to avoid breaking if the format changes, unparsable
|
||||||
|
- lines are passed through unchanged. -}
|
||||||
|
type HookLine = Either String FetchedRef
|
||||||
|
|
||||||
|
{- Runs the hook, allowing lines to be mutated, but never be discarded.
|
||||||
|
- Returns same FetchedRefs that are output by the hook, for further use. -}
|
||||||
|
runHook :: (FetchedRef -> IO FetchedRef) -> IO [FetchedRef]
|
||||||
|
runHook mutate = do
|
||||||
|
ls <- mapM go =<< input
|
||||||
|
output ls
|
||||||
|
|
||||||
|
-- Nothing more should be output to stdout; only hook output
|
||||||
|
-- is accepted by git. Redirect stdout to stderr.
|
||||||
|
hFlush stdout
|
||||||
|
_ <- liftIO $ dupTo stdError stdOutput
|
||||||
|
|
||||||
|
return $ rights ls
|
||||||
|
where
|
||||||
|
go u@(Left _) = return u
|
||||||
|
go (Right r) = Right <$> catchDefaultIO (mutate r) r
|
||||||
|
|
||||||
|
input :: IO [HookLine]
|
||||||
|
input = map parseLine . lines <$> getContents
|
||||||
|
|
||||||
|
output :: [HookLine] -> IO ()
|
||||||
|
output = mapM_ $ putStrLn . genLine
|
||||||
|
|
||||||
|
parseLine :: String -> HookLine
|
||||||
|
parseLine line = go $ words line
|
||||||
|
where
|
||||||
|
go [s, m, r, l]
|
||||||
|
| not $ isSha s = Left line
|
||||||
|
| m == "merge" = parsed True
|
||||||
|
| m == "not-for-merge" = parsed False
|
||||||
|
| otherwise = Left line
|
||||||
|
where
|
||||||
|
parsed v = Right $ FetchedRef
|
||||||
|
{ sha = Ref s
|
||||||
|
, merge = v
|
||||||
|
, remote = Ref r
|
||||||
|
, local = Ref l
|
||||||
|
}
|
||||||
|
go _ = Left line
|
||||||
|
|
||||||
|
genLine :: HookLine -> String
|
||||||
|
genLine (Left l) = l
|
||||||
|
genLine (Right r) = unwords
|
||||||
|
[ show $ sha r
|
||||||
|
, if merge r then "merge" else "not-for-merge"
|
||||||
|
, show $ remote r
|
||||||
|
, show $ local r
|
||||||
|
]
|
36
Git/Types.hs
Normal file
36
Git/Types.hs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{- git data types
|
||||||
|
-
|
||||||
|
- Copyright 2010,2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Types where
|
||||||
|
|
||||||
|
import Network.URI
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
{- There are two types of repositories; those on local disk and those
|
||||||
|
- accessed via an URL. -}
|
||||||
|
data RepoLocation = Dir FilePath | Url URI | Unknown
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
data Repo = Repo {
|
||||||
|
location :: RepoLocation,
|
||||||
|
config :: M.Map String String,
|
||||||
|
remotes :: [Repo],
|
||||||
|
-- remoteName holds the name used for this repo in remotes
|
||||||
|
remoteName :: Maybe String
|
||||||
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
{- A git ref. Can be a sha1, or a branch or tag name. -}
|
||||||
|
newtype Ref = Ref String
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
instance Show Ref where
|
||||||
|
show (Ref v) = v
|
||||||
|
|
||||||
|
{- Aliases for Ref. -}
|
||||||
|
type Branch = Ref
|
||||||
|
type Sha = Ref
|
||||||
|
type Tag = Ref
|
141
Git/UnionMerge.hs
Normal file
141
Git/UnionMerge.hs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
{- git-union-merge library
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.UnionMerge (
|
||||||
|
merge,
|
||||||
|
merge_index,
|
||||||
|
update_index,
|
||||||
|
stream_update_index,
|
||||||
|
update_index_line,
|
||||||
|
ls_tree
|
||||||
|
) where
|
||||||
|
|
||||||
|
import System.Cmd.Utils
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as L
|
||||||
|
import qualified Data.Set as S
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git
|
||||||
|
import Git.Sha
|
||||||
|
import Git.CatFile
|
||||||
|
import Git.Command
|
||||||
|
|
||||||
|
type Streamer = (String -> IO ()) -> IO ()
|
||||||
|
|
||||||
|
{- Performs a union merge between two branches, staging it in the index.
|
||||||
|
- Any previously staged changes in the index will be lost.
|
||||||
|
-
|
||||||
|
- Should be run with a temporary index file configured by useIndex.
|
||||||
|
-}
|
||||||
|
merge :: Ref -> Ref -> Repo -> IO ()
|
||||||
|
merge x y repo = do
|
||||||
|
h <- catFileStart repo
|
||||||
|
stream_update_index repo
|
||||||
|
[ ls_tree x repo
|
||||||
|
, merge_trees x y h repo
|
||||||
|
]
|
||||||
|
catFileStop h
|
||||||
|
|
||||||
|
{- Merges a list of branches into the index. Previously staged changed in
|
||||||
|
- the index are preserved (and participate in the merge). -}
|
||||||
|
merge_index :: CatFileHandle -> Repo -> [Ref] -> IO ()
|
||||||
|
merge_index h repo bs =
|
||||||
|
stream_update_index repo $ map (\b -> merge_tree_index b h repo) bs
|
||||||
|
|
||||||
|
{- Feeds content into update-index. Later items in the list can override
|
||||||
|
- earlier ones, so the list can be generated from any combination of
|
||||||
|
- ls_tree, merge_trees, and merge_tree_index. -}
|
||||||
|
update_index :: Repo -> [String] -> IO ()
|
||||||
|
update_index repo ls = stream_update_index repo [(`mapM_` ls)]
|
||||||
|
|
||||||
|
{- Streams content into update-index. -}
|
||||||
|
stream_update_index :: Repo -> [Streamer] -> IO ()
|
||||||
|
stream_update_index repo as = do
|
||||||
|
(p, h) <- hPipeTo "git" (toCommand $ gitCommandLine params repo)
|
||||||
|
forM_ as (stream h)
|
||||||
|
hClose h
|
||||||
|
forceSuccess p
|
||||||
|
where
|
||||||
|
params = map Param ["update-index", "-z", "--index-info"]
|
||||||
|
stream h a = a (streamer h)
|
||||||
|
streamer h s = do
|
||||||
|
hPutStr h s
|
||||||
|
hPutStr h "\0"
|
||||||
|
|
||||||
|
{- Generates a line suitable to be fed into update-index, to add
|
||||||
|
- a given file with a given sha. -}
|
||||||
|
update_index_line :: Sha -> FilePath -> String
|
||||||
|
update_index_line sha file = "100644 blob " ++ show sha ++ "\t" ++ file
|
||||||
|
|
||||||
|
{- Gets the current tree for a ref. -}
|
||||||
|
ls_tree :: Ref -> Repo -> Streamer
|
||||||
|
ls_tree (Ref x) repo streamer = mapM_ streamer =<< pipeNullSplit params repo
|
||||||
|
where
|
||||||
|
params = map Param ["ls-tree", "-z", "-r", "--full-tree", x]
|
||||||
|
|
||||||
|
{- For merging two trees. -}
|
||||||
|
merge_trees :: Ref -> Ref -> CatFileHandle -> Repo -> Streamer
|
||||||
|
merge_trees (Ref x) (Ref y) h = calc_merge h $ "diff-tree":diff_opts ++ [x, y]
|
||||||
|
|
||||||
|
{- For merging a single tree into the index. -}
|
||||||
|
merge_tree_index :: Ref -> CatFileHandle -> Repo -> Streamer
|
||||||
|
merge_tree_index (Ref x) h = calc_merge h $ "diff-index":diff_opts ++ ["--cached", x]
|
||||||
|
|
||||||
|
diff_opts :: [String]
|
||||||
|
diff_opts = ["--raw", "-z", "-r", "--no-renames", "-l0"]
|
||||||
|
|
||||||
|
{- Calculates how to perform a merge, using git to get a raw diff,
|
||||||
|
- and returning a list suitable for update_index. -}
|
||||||
|
calc_merge :: CatFileHandle -> [String] -> Repo -> Streamer
|
||||||
|
calc_merge ch differ repo streamer = gendiff >>= go
|
||||||
|
where
|
||||||
|
gendiff = pipeNullSplit (map Param differ) repo
|
||||||
|
go [] = return ()
|
||||||
|
go (info:file:rest) = mergeFile info file ch repo >>=
|
||||||
|
maybe (go rest) (\l -> streamer l >> go rest)
|
||||||
|
go (_:[]) = error "calc_merge parse error"
|
||||||
|
|
||||||
|
{- Given an info line from a git raw diff, and the filename, generates
|
||||||
|
- a line suitable for update_index that union merges the two sides of the
|
||||||
|
- diff. -}
|
||||||
|
mergeFile :: String -> FilePath -> CatFileHandle -> Repo -> IO (Maybe String)
|
||||||
|
mergeFile info file h repo = case filter (/= nullsha) [Ref asha, Ref bsha] of
|
||||||
|
[] -> return Nothing
|
||||||
|
(sha:[]) -> use sha
|
||||||
|
shas -> use =<< either return (hashObject repo . L.unlines) =<<
|
||||||
|
calcMerge . zip shas <$> mapM getcontents shas
|
||||||
|
where
|
||||||
|
[_colonmode, _bmode, asha, bsha, _status] = words info
|
||||||
|
nullsha = Ref $ replicate shaSize '0'
|
||||||
|
getcontents s = L.lines <$> catObject h s
|
||||||
|
use sha = return $ Just $ update_index_line sha file
|
||||||
|
|
||||||
|
{- Injects some content into git, returning its Sha. -}
|
||||||
|
hashObject :: Repo -> L.ByteString -> IO Sha
|
||||||
|
hashObject repo content = getSha subcmd $ do
|
||||||
|
(h, s) <- pipeWriteRead (map Param params) content repo
|
||||||
|
L.length s `seq` do
|
||||||
|
forceSuccess h
|
||||||
|
reap -- XXX unsure why this is needed
|
||||||
|
return $ L.unpack s
|
||||||
|
where
|
||||||
|
subcmd = "hash-object"
|
||||||
|
params = [subcmd, "-w", "--stdin"]
|
||||||
|
|
||||||
|
{- Calculates a union merge between a list of refs, with contents.
|
||||||
|
-
|
||||||
|
- When possible, reuses the content of an existing ref, rather than
|
||||||
|
- generating new content.
|
||||||
|
-}
|
||||||
|
calcMerge :: [(Ref, [L.ByteString])] -> Either Ref [L.ByteString]
|
||||||
|
calcMerge shacontents
|
||||||
|
| null reuseable = Right $ new
|
||||||
|
| otherwise = Left $ fst $ Prelude.head reuseable
|
||||||
|
where
|
||||||
|
reuseable = filter (\c -> sorteduniq (snd c) == new) shacontents
|
||||||
|
new = sorteduniq $ concat $ map snd shacontents
|
||||||
|
sorteduniq = S.toList . S.fromList
|
70
Git/Url.hs
Normal file
70
Git/Url.hs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{- git repository urls
|
||||||
|
-
|
||||||
|
- Copyright 2010, 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Url (
|
||||||
|
scheme,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
hostuser,
|
||||||
|
authority,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Network.URI hiding (scheme, authority)
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Git.Types
|
||||||
|
import Git
|
||||||
|
|
||||||
|
{- Scheme of an URL repo. -}
|
||||||
|
scheme :: Repo -> String
|
||||||
|
scheme Repo { location = Url u } = uriScheme u
|
||||||
|
scheme repo = notUrl repo
|
||||||
|
|
||||||
|
{- Work around a bug in the real uriRegName
|
||||||
|
- <http://trac.haskell.org/network/ticket/40> -}
|
||||||
|
uriRegName' :: URIAuth -> String
|
||||||
|
uriRegName' a = fixup $ uriRegName a
|
||||||
|
where
|
||||||
|
fixup x@('[':rest)
|
||||||
|
| rest !! len == ']' = take len rest
|
||||||
|
| otherwise = x
|
||||||
|
where
|
||||||
|
len = length rest - 1
|
||||||
|
fixup x = x
|
||||||
|
|
||||||
|
{- Hostname of an URL repo. -}
|
||||||
|
host :: Repo -> String
|
||||||
|
host = authpart uriRegName'
|
||||||
|
|
||||||
|
{- Port of an URL repo, if it has a nonstandard one. -}
|
||||||
|
port :: Repo -> Maybe Integer
|
||||||
|
port r =
|
||||||
|
case authpart uriPort r of
|
||||||
|
":" -> Nothing
|
||||||
|
(':':p) -> readMaybe p
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
{- Hostname of an URL repo, including any username (ie, "user@host") -}
|
||||||
|
hostuser :: Repo -> String
|
||||||
|
hostuser r = authpart uriUserInfo r ++ authpart uriRegName' r
|
||||||
|
|
||||||
|
{- The full authority portion an URL repo. (ie, "user@host:port") -}
|
||||||
|
authority :: Repo -> String
|
||||||
|
authority = authpart assemble
|
||||||
|
where
|
||||||
|
assemble a = uriUserInfo a ++ uriRegName' a ++ uriPort a
|
||||||
|
|
||||||
|
{- Applies a function to extract part of the uriAuthority of an URL repo. -}
|
||||||
|
authpart :: (URIAuth -> a) -> Repo -> a
|
||||||
|
authpart a Repo { location = Url u } = a auth
|
||||||
|
where
|
||||||
|
auth = fromMaybe (error $ "bad url " ++ show u) (uriAuthority u)
|
||||||
|
authpart _ repo = notUrl repo
|
||||||
|
|
||||||
|
notUrl :: Repo -> a
|
||||||
|
notUrl repo = error $
|
||||||
|
"acting on local git repo " ++ repoDescribe repo ++ " not supported"
|
38
Git/Version.hs
Normal file
38
Git/Version.hs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{- git version checking
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Git.Version where
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import qualified Build.SysConfig
|
||||||
|
|
||||||
|
{- Using the version it was configured for avoids running git to check its
|
||||||
|
- version, at the cost that upgrading git won't be noticed.
|
||||||
|
- This is only acceptable because it's rare that git's version influences
|
||||||
|
- code's behavior. -}
|
||||||
|
version :: String
|
||||||
|
version = Build.SysConfig.gitversion
|
||||||
|
|
||||||
|
older :: String -> Bool
|
||||||
|
older v = normalize version < normalize v
|
||||||
|
|
||||||
|
{- To compare dotted versions like 1.7.7 and 1.8, they are normalized to
|
||||||
|
- a somewhat arbitrary integer representation. -}
|
||||||
|
normalize :: String -> Integer
|
||||||
|
normalize = sum . mult 1 . reverse .
|
||||||
|
extend precision . take precision .
|
||||||
|
map readi . split "."
|
||||||
|
where
|
||||||
|
extend n l = l ++ replicate (n - length l) 0
|
||||||
|
mult _ [] = []
|
||||||
|
mult n (x:xs) = (n*x) : mult (n*10^width) xs
|
||||||
|
readi :: String -> Integer
|
||||||
|
readi s = case reads s of
|
||||||
|
((x,_):_) -> x
|
||||||
|
_ -> 0
|
||||||
|
precision = 10 -- number of segments of the version to compare
|
||||||
|
width = length "yyyymmddhhmmss" -- maximum width of a segment
|
144
GitAnnex.hs
Normal file
144
GitAnnex.hs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
{- git-annex main program
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module GitAnnex where
|
||||||
|
|
||||||
|
import System.Console.GetOpt
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Git.Config
|
||||||
|
import qualified Git.Construct
|
||||||
|
import CmdLine
|
||||||
|
import Command
|
||||||
|
import Types.TrustLevel
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Limit
|
||||||
|
import qualified Utility.Format
|
||||||
|
|
||||||
|
import qualified Command.Add
|
||||||
|
import qualified Command.Unannex
|
||||||
|
import qualified Command.Drop
|
||||||
|
import qualified Command.Move
|
||||||
|
import qualified Command.Copy
|
||||||
|
import qualified Command.Get
|
||||||
|
import qualified Command.FromKey
|
||||||
|
import qualified Command.DropKey
|
||||||
|
import qualified Command.Reinject
|
||||||
|
import qualified Command.Fix
|
||||||
|
import qualified Command.Init
|
||||||
|
import qualified Command.Describe
|
||||||
|
import qualified Command.InitRemote
|
||||||
|
import qualified Command.Fsck
|
||||||
|
import qualified Command.Unused
|
||||||
|
import qualified Command.DropUnused
|
||||||
|
import qualified Command.Unlock
|
||||||
|
import qualified Command.Lock
|
||||||
|
import qualified Command.PreCommit
|
||||||
|
import qualified Command.TweakFetch
|
||||||
|
import qualified Command.Find
|
||||||
|
import qualified Command.Whereis
|
||||||
|
import qualified Command.Merge
|
||||||
|
import qualified Command.Status
|
||||||
|
import qualified Command.Migrate
|
||||||
|
import qualified Command.Uninit
|
||||||
|
import qualified Command.Trust
|
||||||
|
import qualified Command.Untrust
|
||||||
|
import qualified Command.Semitrust
|
||||||
|
import qualified Command.Dead
|
||||||
|
import qualified Command.Sync
|
||||||
|
import qualified Command.AddUrl
|
||||||
|
import qualified Command.Map
|
||||||
|
import qualified Command.Upgrade
|
||||||
|
import qualified Command.Version
|
||||||
|
|
||||||
|
cmds :: [Command]
|
||||||
|
cmds = concat
|
||||||
|
[ Command.Add.def
|
||||||
|
, Command.Get.def
|
||||||
|
, Command.Drop.def
|
||||||
|
, Command.Move.def
|
||||||
|
, Command.Copy.def
|
||||||
|
, Command.Unlock.def
|
||||||
|
, Command.Lock.def
|
||||||
|
, Command.Sync.def
|
||||||
|
, Command.AddUrl.def
|
||||||
|
, Command.Init.def
|
||||||
|
, Command.Describe.def
|
||||||
|
, Command.InitRemote.def
|
||||||
|
, Command.Reinject.def
|
||||||
|
, Command.Unannex.def
|
||||||
|
, Command.Uninit.def
|
||||||
|
, Command.PreCommit.def
|
||||||
|
, Command.TweakFetch.def
|
||||||
|
, Command.Trust.def
|
||||||
|
, Command.Untrust.def
|
||||||
|
, Command.Semitrust.def
|
||||||
|
, Command.Dead.def
|
||||||
|
, Command.FromKey.def
|
||||||
|
, Command.DropKey.def
|
||||||
|
, Command.Fix.def
|
||||||
|
, Command.Fsck.def
|
||||||
|
, Command.Unused.def
|
||||||
|
, Command.DropUnused.def
|
||||||
|
, Command.Find.def
|
||||||
|
, Command.Whereis.def
|
||||||
|
, Command.Merge.def
|
||||||
|
, Command.Status.def
|
||||||
|
, Command.Migrate.def
|
||||||
|
, Command.Map.def
|
||||||
|
, Command.Upgrade.def
|
||||||
|
, Command.Version.def
|
||||||
|
]
|
||||||
|
|
||||||
|
options :: [Option]
|
||||||
|
options = commonOptions ++
|
||||||
|
[ Option ['t'] ["to"] (ReqArg setto paramRemote)
|
||||||
|
"specify to where to transfer content"
|
||||||
|
, Option ['f'] ["from"] (ReqArg setfrom paramRemote)
|
||||||
|
"specify from where to transfer content"
|
||||||
|
, Option ['N'] ["numcopies"] (ReqArg setnumcopies paramNumber)
|
||||||
|
"override default number of copies"
|
||||||
|
, Option [] ["trust"] (ReqArg (Remote.forceTrust Trusted) paramRemote)
|
||||||
|
"override trust setting"
|
||||||
|
, Option [] ["semitrust"] (ReqArg (Remote.forceTrust SemiTrusted) paramRemote)
|
||||||
|
"override trust setting back to default"
|
||||||
|
, Option [] ["untrust"] (ReqArg (Remote.forceTrust UnTrusted) paramRemote)
|
||||||
|
"override trust setting to untrusted"
|
||||||
|
, Option ['c'] ["config"] (ReqArg setgitconfig "NAME=VALUE")
|
||||||
|
"override git configuration setting"
|
||||||
|
, Option [] ["print0"] (NoArg setprint0)
|
||||||
|
"terminate output with null"
|
||||||
|
, Option [] ["format"] (ReqArg setformat paramFormat)
|
||||||
|
"control format of output"
|
||||||
|
, Option ['x'] ["exclude"] (ReqArg Limit.addExclude paramGlob)
|
||||||
|
"skip files matching the glob pattern"
|
||||||
|
, Option ['I'] ["include"] (ReqArg Limit.addInclude paramGlob)
|
||||||
|
"don't skip files matching the glob pattern"
|
||||||
|
, Option ['i'] ["in"] (ReqArg Limit.addIn paramRemote)
|
||||||
|
"skip files not present in a remote"
|
||||||
|
, Option ['C'] ["copies"] (ReqArg Limit.addCopies paramNumber)
|
||||||
|
"skip files with fewer copies"
|
||||||
|
, Option ['B'] ["inbackend"] (ReqArg Limit.addInBackend paramName)
|
||||||
|
"skip files not using a key-value backend"
|
||||||
|
] ++ matcherOptions
|
||||||
|
where
|
||||||
|
setto v = Annex.changeState $ \s -> s { Annex.toremote = Just v }
|
||||||
|
setfrom v = Annex.changeState $ \s -> s { Annex.fromremote = Just v }
|
||||||
|
setnumcopies v = Annex.changeState $ \s -> s {Annex.forcenumcopies = readMaybe v }
|
||||||
|
setformat v = Annex.changeState $ \s -> s { Annex.format = Just $ Utility.Format.gen v }
|
||||||
|
setprint0 = setformat "${file}\0"
|
||||||
|
setgitconfig :: String -> Annex ()
|
||||||
|
setgitconfig v = do
|
||||||
|
newg <- inRepo $ Git.Config.store v
|
||||||
|
Annex.changeState $ \s -> s { Annex.repo = newg }
|
||||||
|
|
||||||
|
header :: String
|
||||||
|
header = "Usage: git-annex command [option ..]"
|
||||||
|
|
||||||
|
run :: [String] -> IO ()
|
||||||
|
run args = dispatch args cmds options header Git.Construct.fromCwd
|
1
INSTALL
Symbolic link
1
INSTALL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
doc/install.mdwn
|
83
Init.hs
Normal file
83
Init.hs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{- git-annex repository initialization
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Init (
|
||||||
|
ensureInitialized,
|
||||||
|
initialize,
|
||||||
|
uninitialize
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Utility.TempFile
|
||||||
|
import qualified Git
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import Logs.UUID
|
||||||
|
import Annex.Version
|
||||||
|
import Annex.UUID
|
||||||
|
|
||||||
|
initialize :: Maybe String -> Annex ()
|
||||||
|
initialize mdescription = do
|
||||||
|
prepUUID
|
||||||
|
Annex.Branch.create
|
||||||
|
setVersion
|
||||||
|
gitHooksWrite
|
||||||
|
u <- getUUID
|
||||||
|
maybe (recordUUID u) (describeUUID u) mdescription
|
||||||
|
|
||||||
|
uninitialize :: Annex ()
|
||||||
|
uninitialize = gitHooksUnWrite
|
||||||
|
|
||||||
|
{- Will automatically initialize if there is already a git-annex
|
||||||
|
branch from somewhere. Otherwise, require a manual init
|
||||||
|
to avoid git-annex accidentially being run in git
|
||||||
|
repos that did not intend to use it. -}
|
||||||
|
ensureInitialized :: Annex ()
|
||||||
|
ensureInitialized = getVersion >>= maybe needsinit checkVersion
|
||||||
|
where
|
||||||
|
needsinit = do
|
||||||
|
annexed <- Annex.Branch.hasSibling
|
||||||
|
if annexed
|
||||||
|
then initialize Nothing
|
||||||
|
else error "First run: git-annex init"
|
||||||
|
|
||||||
|
{- set up git hooks, if not already present -}
|
||||||
|
gitHooksWrite :: Annex ()
|
||||||
|
gitHooksWrite = unlessBare $ forM_ hooks $ \(hook, content) -> do
|
||||||
|
file <- hookFile hook
|
||||||
|
exists <- liftIO $ doesFileExist file
|
||||||
|
if exists
|
||||||
|
then warning $ hook ++ " hook (" ++ file ++ ") already exists, not configuring"
|
||||||
|
else liftIO $ do
|
||||||
|
viaTmp writeFile file content
|
||||||
|
p <- getPermissions file
|
||||||
|
setPermissions file $ p {executable = True}
|
||||||
|
|
||||||
|
gitHooksUnWrite :: Annex ()
|
||||||
|
gitHooksUnWrite = unlessBare $ forM_ hooks $ \(hook, content) -> do
|
||||||
|
file <- hookFile hook
|
||||||
|
whenM (liftIO $ doesFileExist file) $ do
|
||||||
|
c <- liftIO $ readFile file
|
||||||
|
if c == content
|
||||||
|
then liftIO $ removeFile file
|
||||||
|
else warning $ hook ++ " hook (" ++ file ++
|
||||||
|
") contents modified; not deleting." ++
|
||||||
|
" Edit it to remove call to git annex."
|
||||||
|
|
||||||
|
unlessBare :: Annex () -> Annex ()
|
||||||
|
unlessBare = unlessM $ fromRepo $ Git.repoIsLocalBare
|
||||||
|
|
||||||
|
hookFile :: FilePath -> Annex FilePath
|
||||||
|
hookFile f = (</>) <$> fromRepo Git.gitDir <*> pure ("hooks/" ++ f)
|
||||||
|
|
||||||
|
hooks :: [(String, String)]
|
||||||
|
hooks = [ ("pre-commit", hookscript "git annex pre-commit .")
|
||||||
|
, ("tweak-fetch", hookscript "git annex tweak-fetch")
|
||||||
|
]
|
||||||
|
where
|
||||||
|
hookscript s = "#!/bin/sh\n" ++
|
||||||
|
"# automatically configured by git-annex\n" ++
|
||||||
|
s ++ "\n";
|
103
Limit.hs
Normal file
103
Limit.hs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
{- user-specified limits on files to act on
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Limit where
|
||||||
|
|
||||||
|
import Text.Regex.PCRE.Light.Char8
|
||||||
|
import System.Path.WildMatch
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex
|
||||||
|
import qualified Utility.Matcher
|
||||||
|
import qualified Remote
|
||||||
|
import qualified Backend
|
||||||
|
import Logs.Location
|
||||||
|
import Annex.Content
|
||||||
|
|
||||||
|
type Limit = Utility.Matcher.Token (FilePath -> Annex Bool)
|
||||||
|
|
||||||
|
{- Checks if there are user-specified limits. -}
|
||||||
|
limited :: Annex Bool
|
||||||
|
limited = (not . Utility.Matcher.matchesAny) <$> getMatcher'
|
||||||
|
|
||||||
|
{- Gets a matcher for the user-specified limits. The matcher is cached for
|
||||||
|
- speed; once it's obtained the user-specified limits can't change. -}
|
||||||
|
getMatcher :: Annex (FilePath -> Annex Bool)
|
||||||
|
getMatcher = Utility.Matcher.matchM <$> getMatcher'
|
||||||
|
|
||||||
|
getMatcher' :: Annex (Utility.Matcher.Matcher (FilePath -> Annex Bool))
|
||||||
|
getMatcher' = do
|
||||||
|
m <- Annex.getState Annex.limit
|
||||||
|
case m of
|
||||||
|
Right r -> return r
|
||||||
|
Left l -> do
|
||||||
|
let matcher = Utility.Matcher.generate (reverse l)
|
||||||
|
Annex.changeState $ \s -> s { Annex.limit = Right matcher }
|
||||||
|
return matcher
|
||||||
|
|
||||||
|
{- Adds something to the limit list, which is built up reversed. -}
|
||||||
|
add :: Limit -> Annex ()
|
||||||
|
add l = Annex.changeState $ \s -> s { Annex.limit = prepend $ Annex.limit s }
|
||||||
|
where
|
||||||
|
prepend (Left ls) = Left $ l:ls
|
||||||
|
prepend _ = error "internal"
|
||||||
|
|
||||||
|
{- Adds a new token. -}
|
||||||
|
addToken :: String -> Annex ()
|
||||||
|
addToken = add . Utility.Matcher.token
|
||||||
|
|
||||||
|
{- Adds a new limit. -}
|
||||||
|
addLimit :: (FilePath -> Annex Bool) -> Annex ()
|
||||||
|
addLimit = add . Utility.Matcher.Operation
|
||||||
|
|
||||||
|
{- Add a limit to skip files that do not match the glob. -}
|
||||||
|
addInclude :: String -> Annex ()
|
||||||
|
addInclude glob = addLimit $ return . matchglob glob
|
||||||
|
|
||||||
|
{- Add a limit to skip files that match the glob. -}
|
||||||
|
addExclude :: String -> Annex ()
|
||||||
|
addExclude glob = addLimit $ return . not . matchglob glob
|
||||||
|
|
||||||
|
matchglob :: String -> FilePath -> Bool
|
||||||
|
matchglob glob f = isJust $ match cregex f []
|
||||||
|
where
|
||||||
|
cregex = compile regex []
|
||||||
|
regex = '^':wildToRegex glob
|
||||||
|
|
||||||
|
{- Adds a limit to skip files not believed to be present
|
||||||
|
- in a specfied repository. -}
|
||||||
|
addIn :: String -> Annex ()
|
||||||
|
addIn name = addLimit $ check $ if name == "." then inAnnex else inremote
|
||||||
|
where
|
||||||
|
check a = Backend.lookupFile >=> handle a
|
||||||
|
handle _ Nothing = return False
|
||||||
|
handle a (Just (key, _)) = a key
|
||||||
|
inremote key = do
|
||||||
|
u <- Remote.nameToUUID name
|
||||||
|
us <- keyLocations key
|
||||||
|
return $ u `elem` us
|
||||||
|
|
||||||
|
{- Adds a limit to skip files not believed to have the specified number
|
||||||
|
- of copies. -}
|
||||||
|
addCopies :: String -> Annex ()
|
||||||
|
addCopies num =
|
||||||
|
case readMaybe num :: Maybe Int of
|
||||||
|
Nothing -> error "bad number for --copies"
|
||||||
|
Just n -> addLimit $ check n
|
||||||
|
where
|
||||||
|
check n = Backend.lookupFile >=> handle n
|
||||||
|
handle _ Nothing = return False
|
||||||
|
handle n (Just (key, _)) = do
|
||||||
|
us <- keyLocations key
|
||||||
|
return $ length us >= n
|
||||||
|
|
||||||
|
{- Adds a limit to skip files not using a specified key-value backend. -}
|
||||||
|
addInBackend :: String -> Annex ()
|
||||||
|
addInBackend name = addLimit $ Backend.lookupFile >=> check
|
||||||
|
where
|
||||||
|
wanted = Backend.lookupBackendName name
|
||||||
|
check = return . maybe False ((==) wanted . snd)
|
228
Locations.hs
Normal file
228
Locations.hs
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
{- git-annex file locations
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Locations (
|
||||||
|
keyFile,
|
||||||
|
fileKey,
|
||||||
|
keyPaths,
|
||||||
|
gitAnnexLocation,
|
||||||
|
annexLocations,
|
||||||
|
gitAnnexDir,
|
||||||
|
gitAnnexObjectDir,
|
||||||
|
gitAnnexTmpDir,
|
||||||
|
gitAnnexTmpLocation,
|
||||||
|
gitAnnexBadDir,
|
||||||
|
gitAnnexBadLocation,
|
||||||
|
gitAnnexUnusedLog,
|
||||||
|
gitAnnexJournalDir,
|
||||||
|
gitAnnexJournalLock,
|
||||||
|
gitAnnexIndex,
|
||||||
|
gitAnnexIndexLock,
|
||||||
|
isLinkToAnnex,
|
||||||
|
annexHashes,
|
||||||
|
hashDirMixed,
|
||||||
|
hashDirLower,
|
||||||
|
|
||||||
|
prop_idempotent_fileKey
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Bits
|
||||||
|
import Data.Word
|
||||||
|
import Data.Hash.MD5
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Types
|
||||||
|
import Types.Key
|
||||||
|
import qualified Git
|
||||||
|
|
||||||
|
{- Conventions:
|
||||||
|
-
|
||||||
|
- Functions ending in "Dir" should always return values ending with a
|
||||||
|
- trailing path separator. Most code does not rely on that, but a few
|
||||||
|
- things do.
|
||||||
|
-
|
||||||
|
- Everything else should not end in a trailing path sepatator.
|
||||||
|
-
|
||||||
|
- Only functions (with names starting with "git") that build a path
|
||||||
|
- based on a git repository should return an absolute path.
|
||||||
|
- Everything else should use relative paths.
|
||||||
|
-}
|
||||||
|
|
||||||
|
{- The directory git annex uses for local state, relative to the .git
|
||||||
|
- directory -}
|
||||||
|
annexDir :: FilePath
|
||||||
|
annexDir = addTrailingPathSeparator "annex"
|
||||||
|
|
||||||
|
{- The directory git annex uses for locally available object content,
|
||||||
|
- relative to the .git directory -}
|
||||||
|
objectDir :: FilePath
|
||||||
|
objectDir = addTrailingPathSeparator $ annexDir </> "objects"
|
||||||
|
|
||||||
|
{- Annexed file's possible locations relative to the .git directory.
|
||||||
|
- There are two different possibilities, using different hashes. -}
|
||||||
|
annexLocations :: Key -> [FilePath]
|
||||||
|
annexLocations key = map (annexLocation key) annexHashes
|
||||||
|
annexLocation :: Key -> Hasher -> FilePath
|
||||||
|
annexLocation key hasher = objectDir </> keyPath key hasher
|
||||||
|
|
||||||
|
{- Annexed file's absolute location in a repository.
|
||||||
|
-
|
||||||
|
- When there are multiple possible locations, returns the one where the
|
||||||
|
- file is actually present.
|
||||||
|
-
|
||||||
|
- When the file is not present, returns the location where the file should
|
||||||
|
- be stored.
|
||||||
|
-}
|
||||||
|
gitAnnexLocation :: Key -> Git.Repo -> IO FilePath
|
||||||
|
gitAnnexLocation key r
|
||||||
|
| Git.repoIsLocalBare r =
|
||||||
|
{- Bare repositories default to hashDirLower for new
|
||||||
|
- content, as it's more portable. -}
|
||||||
|
check (map inrepo $ annexLocations key)
|
||||||
|
| otherwise =
|
||||||
|
{- Non-bare repositories only use hashDirMixed, so
|
||||||
|
- don't need to do any work to check if the file is
|
||||||
|
- present. -}
|
||||||
|
return $ inrepo ".git" </> annexLocation key hashDirMixed
|
||||||
|
where
|
||||||
|
inrepo d = Git.workTree r </> d
|
||||||
|
check locs@(l:_) = fromMaybe l <$> firstM doesFileExist locs
|
||||||
|
check [] = error "internal"
|
||||||
|
|
||||||
|
{- The annex directory of a repository. -}
|
||||||
|
gitAnnexDir :: Git.Repo -> FilePath
|
||||||
|
gitAnnexDir r
|
||||||
|
| Git.repoIsLocalBare r = addTrailingPathSeparator $ Git.workTree r </> annexDir
|
||||||
|
| otherwise = addTrailingPathSeparator $ Git.workTree r </> ".git" </> annexDir
|
||||||
|
|
||||||
|
{- The part of the annex directory where file contents are stored. -}
|
||||||
|
gitAnnexObjectDir :: Git.Repo -> FilePath
|
||||||
|
gitAnnexObjectDir r
|
||||||
|
| Git.repoIsLocalBare r = addTrailingPathSeparator $ Git.workTree r </> objectDir
|
||||||
|
| otherwise = addTrailingPathSeparator $ Git.workTree r </> ".git" </> objectDir
|
||||||
|
|
||||||
|
{- .git/annex/tmp/ is used for temp files -}
|
||||||
|
gitAnnexTmpDir :: Git.Repo -> FilePath
|
||||||
|
gitAnnexTmpDir r = addTrailingPathSeparator $ gitAnnexDir r </> "tmp"
|
||||||
|
|
||||||
|
{- The temp file to use for a given key. -}
|
||||||
|
gitAnnexTmpLocation :: Key -> Git.Repo -> FilePath
|
||||||
|
gitAnnexTmpLocation key r = gitAnnexTmpDir r </> keyFile key
|
||||||
|
|
||||||
|
{- .git/annex/bad/ is used for bad files found during fsck -}
|
||||||
|
gitAnnexBadDir :: Git.Repo -> FilePath
|
||||||
|
gitAnnexBadDir r = addTrailingPathSeparator $ gitAnnexDir r </> "bad"
|
||||||
|
|
||||||
|
{- The bad file to use for a given key. -}
|
||||||
|
gitAnnexBadLocation :: Key -> Git.Repo -> FilePath
|
||||||
|
gitAnnexBadLocation key r = gitAnnexBadDir r </> keyFile key
|
||||||
|
|
||||||
|
{- .git/annex/*unused is used to number possibly unused keys -}
|
||||||
|
gitAnnexUnusedLog :: FilePath -> Git.Repo -> FilePath
|
||||||
|
gitAnnexUnusedLog prefix r = gitAnnexDir r </> (prefix ++ "unused")
|
||||||
|
|
||||||
|
{- .git/annex/journal/ is used to journal changes made to the git-annex
|
||||||
|
- branch -}
|
||||||
|
gitAnnexJournalDir :: Git.Repo -> FilePath
|
||||||
|
gitAnnexJournalDir r = addTrailingPathSeparator $ gitAnnexDir r </> "journal"
|
||||||
|
|
||||||
|
{- Lock file for the journal. -}
|
||||||
|
gitAnnexJournalLock :: Git.Repo -> FilePath
|
||||||
|
gitAnnexJournalLock r = gitAnnexDir r </> "journal.lck"
|
||||||
|
|
||||||
|
{- .git/annex/index is used to stage changes to the git-annex branch -}
|
||||||
|
gitAnnexIndex :: Git.Repo -> FilePath
|
||||||
|
gitAnnexIndex r = gitAnnexDir r </> "index"
|
||||||
|
|
||||||
|
{- Lock file for .git/annex/index. -}
|
||||||
|
gitAnnexIndexLock :: Git.Repo -> FilePath
|
||||||
|
gitAnnexIndexLock r = gitAnnexDir r </> "index.lck"
|
||||||
|
|
||||||
|
{- Checks a symlink target to see if it appears to point to annexed content. -}
|
||||||
|
isLinkToAnnex :: FilePath -> Bool
|
||||||
|
isLinkToAnnex s = ("/.git/" ++ objectDir) `isInfixOf` s
|
||||||
|
|
||||||
|
{- Converts a key into a filename fragment without any directory.
|
||||||
|
-
|
||||||
|
- Escape "/" in the key name, to keep a flat tree of files and avoid
|
||||||
|
- issues with keys containing "/../" or ending with "/" etc.
|
||||||
|
-
|
||||||
|
- "/" is escaped to "%" because it's short and rarely used, and resembles
|
||||||
|
- a slash
|
||||||
|
- "%" is escaped to "&s", and "&" to "&a"; this ensures that the mapping
|
||||||
|
- is one to one.
|
||||||
|
- ":" is escaped to "&c", because despite it being 2011, people still care
|
||||||
|
- about FAT.
|
||||||
|
-}
|
||||||
|
keyFile :: Key -> FilePath
|
||||||
|
keyFile key = replace "/" "%" $ replace ":" "&c" $
|
||||||
|
replace "%" "&s" $ replace "&" "&a" $ show key
|
||||||
|
|
||||||
|
{- A location to store a key on the filesystem. A directory hash is used,
|
||||||
|
- to protect against filesystems that dislike having many items in a
|
||||||
|
- single directory.
|
||||||
|
-
|
||||||
|
- The file is put in a directory with the same name, this allows
|
||||||
|
- write-protecting the directory to avoid accidental deletion of the file.
|
||||||
|
-}
|
||||||
|
keyPath :: Key -> Hasher -> FilePath
|
||||||
|
keyPath key hasher = hasher key </> f </> f
|
||||||
|
where
|
||||||
|
f = keyFile key
|
||||||
|
|
||||||
|
{- All possibile locations to store a key using different directory hashes. -}
|
||||||
|
keyPaths :: Key -> [FilePath]
|
||||||
|
keyPaths key = map (keyPath key) annexHashes
|
||||||
|
|
||||||
|
{- Reverses keyFile, converting a filename fragment (ie, the basename of
|
||||||
|
- the symlink target) into a key. -}
|
||||||
|
fileKey :: FilePath -> Maybe Key
|
||||||
|
fileKey file = readKey $
|
||||||
|
replace "&a" "&" $ replace "&s" "%" $
|
||||||
|
replace "&c" ":" $ replace "%" "/" file
|
||||||
|
|
||||||
|
{- for quickcheck -}
|
||||||
|
prop_idempotent_fileKey :: String -> Bool
|
||||||
|
prop_idempotent_fileKey s = Just k == fileKey (keyFile k)
|
||||||
|
where k = stubKey { keyName = s, keyBackendName = "test" }
|
||||||
|
|
||||||
|
{- Two different directory hashes may be used. The mixed case hash
|
||||||
|
- came first, and is fine, except for the problem of case-strict
|
||||||
|
- filesystems such as Linux VFAT (mounted with shortname=mixed),
|
||||||
|
- which do not allow using a directory "XX" when "xx" already exists.
|
||||||
|
- To support that, most repositories use the lower case hash for new data. -}
|
||||||
|
type Hasher = Key -> FilePath
|
||||||
|
annexHashes :: [Hasher]
|
||||||
|
annexHashes = [hashDirLower, hashDirMixed]
|
||||||
|
|
||||||
|
hashDirMixed :: Hasher
|
||||||
|
hashDirMixed k = addTrailingPathSeparator $ take 2 dir </> drop 2 dir
|
||||||
|
where
|
||||||
|
dir = take 4 $ display_32bits_as_dir =<< [a,b,c,d]
|
||||||
|
ABCD (a,b,c,d) = md5 $ Str $ show k
|
||||||
|
|
||||||
|
hashDirLower :: Hasher
|
||||||
|
hashDirLower k = addTrailingPathSeparator $ take 3 dir </> drop 3 dir
|
||||||
|
where
|
||||||
|
dir = take 6 $ md5s $ Str $ show k
|
||||||
|
|
||||||
|
{- modified version of display_32bits_as_hex from Data.Hash.MD5
|
||||||
|
- Copyright (C) 2001 Ian Lynagh
|
||||||
|
- License: Either BSD or GPL
|
||||||
|
-}
|
||||||
|
display_32bits_as_dir :: Word32 -> String
|
||||||
|
display_32bits_as_dir w = trim $ swap_pairs cs
|
||||||
|
where
|
||||||
|
-- Need 32 characters to use. To avoid inaverdently making
|
||||||
|
-- a real word, use letters that appear less frequently.
|
||||||
|
chars = ['0'..'9'] ++ "zqjxkmvwgpfZQJXKMVWGPF"
|
||||||
|
cs = map (\x -> getc $ (shiftR w (6*x)) .&. 31) [0..7]
|
||||||
|
getc n = chars !! fromIntegral n
|
||||||
|
swap_pairs (x1:x2:xs) = x2:x1:swap_pairs xs
|
||||||
|
swap_pairs _ = []
|
||||||
|
-- Last 2 will always be 00, so omit.
|
||||||
|
trim = take 6
|
74
Logs/Location.hs
Normal file
74
Logs/Location.hs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{-# LANGUAGE BangPatterns #-}
|
||||||
|
|
||||||
|
{- git-annex location log
|
||||||
|
-
|
||||||
|
- git-annex keeps track of which repositories have the contents of annexed
|
||||||
|
- files.
|
||||||
|
-
|
||||||
|
- Repositories record their UUID and the date when they --get or --drop
|
||||||
|
- a value.
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Logs.Location (
|
||||||
|
LogStatus(..),
|
||||||
|
logChange,
|
||||||
|
readLog,
|
||||||
|
keyLocations,
|
||||||
|
loggedKeys,
|
||||||
|
loggedKeysFor,
|
||||||
|
logFile,
|
||||||
|
logFileKey
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import Logs.Presence
|
||||||
|
import Logs.Trust
|
||||||
|
|
||||||
|
{- Log a change in the presence of a key's value in a repository. -}
|
||||||
|
logChange :: Key -> UUID -> LogStatus -> Annex ()
|
||||||
|
logChange key (UUID u) s = addLog (logFile key) =<< logNow s u
|
||||||
|
logChange _ NoUUID _ = return ()
|
||||||
|
|
||||||
|
{- Returns a list of repository UUIDs that, according to the log, have
|
||||||
|
- the value of a key.
|
||||||
|
-
|
||||||
|
- Dead repositories are skipped.
|
||||||
|
-}
|
||||||
|
keyLocations :: Key -> Annex [UUID]
|
||||||
|
keyLocations key = do
|
||||||
|
l <- map toUUID <$> (currentLog . logFile) key
|
||||||
|
snd <$> trustPartition DeadTrusted l
|
||||||
|
|
||||||
|
{- Finds all keys that have location log information.
|
||||||
|
- (There may be duplicate keys in the list.) -}
|
||||||
|
loggedKeys :: Annex [Key]
|
||||||
|
loggedKeys = mapMaybe (logFileKey . takeFileName) <$> Annex.Branch.files
|
||||||
|
|
||||||
|
{- Finds all keys that have location log information indicating
|
||||||
|
- they are present for the specified repository. -}
|
||||||
|
loggedKeysFor :: UUID -> Annex [Key]
|
||||||
|
loggedKeysFor u = filterM isthere =<< loggedKeys
|
||||||
|
where
|
||||||
|
{- This should run strictly to avoid the filterM
|
||||||
|
- building many thunks containing keyLocations data. -}
|
||||||
|
isthere k = do
|
||||||
|
us <- keyLocations k
|
||||||
|
let !there = u `elem` us
|
||||||
|
return there
|
||||||
|
|
||||||
|
{- The filename of the log file for a given key. -}
|
||||||
|
logFile :: Key -> String
|
||||||
|
logFile key = hashDirLower key ++ keyFile key ++ ".log"
|
||||||
|
|
||||||
|
{- Converts a log filename into a key. -}
|
||||||
|
logFileKey :: FilePath -> Maybe Key
|
||||||
|
logFileKey file
|
||||||
|
| ext == ".log" = fileKey base
|
||||||
|
| otherwise = Nothing
|
||||||
|
where
|
||||||
|
(base, ext) = splitAt (length file - 4) file
|
104
Logs/Presence.hs
Normal file
104
Logs/Presence.hs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
{- git-annex presence log
|
||||||
|
-
|
||||||
|
- This is used to store presence information in the git-annex branch in
|
||||||
|
- a way that can be union merged.
|
||||||
|
-
|
||||||
|
- A line of the log will look like: "date N INFO"
|
||||||
|
- Where N=1 when the INFO is present, and 0 otherwise.
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Logs.Presence (
|
||||||
|
LogStatus(..),
|
||||||
|
addLog,
|
||||||
|
readLog,
|
||||||
|
parseLog,
|
||||||
|
showLog,
|
||||||
|
logNow,
|
||||||
|
compactLog,
|
||||||
|
currentLog,
|
||||||
|
LogLine
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Time.Clock.POSIX
|
||||||
|
import Data.Time
|
||||||
|
import System.Locale
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex.Branch
|
||||||
|
|
||||||
|
data LogLine = LogLine {
|
||||||
|
date :: POSIXTime,
|
||||||
|
status :: LogStatus,
|
||||||
|
info :: String
|
||||||
|
} deriving (Eq)
|
||||||
|
|
||||||
|
data LogStatus = InfoPresent | InfoMissing
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
addLog :: FilePath -> LogLine -> Annex ()
|
||||||
|
addLog file line = Annex.Branch.change file $ \s ->
|
||||||
|
showLog $ compactLog (line : parseLog s)
|
||||||
|
|
||||||
|
{- Reads a log file.
|
||||||
|
- Note that the LogLines returned may be in any order. -}
|
||||||
|
readLog :: FilePath -> Annex [LogLine]
|
||||||
|
readLog file = parseLog <$> Annex.Branch.get file
|
||||||
|
|
||||||
|
{- Parses a log file. Unparseable lines are ignored. -}
|
||||||
|
parseLog :: String -> [LogLine]
|
||||||
|
parseLog = mapMaybe (parseline . words) . lines
|
||||||
|
where
|
||||||
|
parseline (a:b:c:_) = do
|
||||||
|
d <- parseTime defaultTimeLocale "%s%Qs" a
|
||||||
|
s <- parsestatus b
|
||||||
|
Just $ LogLine (utcTimeToPOSIXSeconds d) s c
|
||||||
|
parseline _ = Nothing
|
||||||
|
parsestatus "1" = Just InfoPresent
|
||||||
|
parsestatus "0" = Just InfoMissing
|
||||||
|
parsestatus _ = Nothing
|
||||||
|
|
||||||
|
{- Generates a log file. -}
|
||||||
|
showLog :: [LogLine] -> String
|
||||||
|
showLog = unlines . map genline
|
||||||
|
where
|
||||||
|
genline (LogLine d s i) = unwords [show d, genstatus s, i]
|
||||||
|
genstatus InfoPresent = "1"
|
||||||
|
genstatus InfoMissing = "0"
|
||||||
|
|
||||||
|
{- Generates a new LogLine with the current date. -}
|
||||||
|
logNow :: LogStatus -> String -> Annex LogLine
|
||||||
|
logNow s i = do
|
||||||
|
now <- liftIO getPOSIXTime
|
||||||
|
return $ LogLine now s i
|
||||||
|
|
||||||
|
{- Reads a log and returns only the info that is still in effect. -}
|
||||||
|
currentLog :: FilePath -> Annex [String]
|
||||||
|
currentLog file = map info . filterPresent <$> readLog file
|
||||||
|
|
||||||
|
{- Returns the info from LogLines that are in effect. -}
|
||||||
|
filterPresent :: [LogLine] -> [LogLine]
|
||||||
|
filterPresent = filter (\l -> InfoPresent == status l) . compactLog
|
||||||
|
|
||||||
|
{- Compacts a set of logs, returning a subset that contains the current
|
||||||
|
- status. -}
|
||||||
|
compactLog :: [LogLine] -> [LogLine]
|
||||||
|
compactLog = M.elems . foldr mapLog M.empty
|
||||||
|
|
||||||
|
type LogMap = M.Map String LogLine
|
||||||
|
|
||||||
|
{- Inserts a log into a map of logs, if the log has better (ie, newer)
|
||||||
|
- information than the other logs in the map -}
|
||||||
|
mapLog :: LogLine -> LogMap -> LogMap
|
||||||
|
mapLog l m =
|
||||||
|
if better
|
||||||
|
then M.insert i l m
|
||||||
|
else m
|
||||||
|
where
|
||||||
|
better = maybe True newer $ M.lookup i m
|
||||||
|
newer l' = date l' <= date l
|
||||||
|
i = info l
|
86
Logs/Remote.hs
Normal file
86
Logs/Remote.hs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
{- git-annex remote log
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Logs.Remote (
|
||||||
|
readRemoteLog,
|
||||||
|
configSet,
|
||||||
|
keyValToConfig,
|
||||||
|
configToKeyVal,
|
||||||
|
|
||||||
|
prop_idempotent_configEscape
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import Data.Time.Clock.POSIX
|
||||||
|
import Data.Char
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import Types.Remote
|
||||||
|
import Logs.UUIDBased
|
||||||
|
|
||||||
|
{- Filename of remote.log. -}
|
||||||
|
remoteLog :: FilePath
|
||||||
|
remoteLog = "remote.log"
|
||||||
|
|
||||||
|
{- Adds or updates a remote's config in the log. -}
|
||||||
|
configSet :: UUID -> RemoteConfig -> Annex ()
|
||||||
|
configSet u c = do
|
||||||
|
ts <- liftIO getPOSIXTime
|
||||||
|
Annex.Branch.change remoteLog $
|
||||||
|
showLog showConfig . changeLog ts u c . parseLog parseConfig
|
||||||
|
|
||||||
|
{- Map of remotes by uuid containing key/value config maps. -}
|
||||||
|
readRemoteLog :: Annex (M.Map UUID RemoteConfig)
|
||||||
|
readRemoteLog = (simpleMap . parseLog parseConfig) <$> Annex.Branch.get remoteLog
|
||||||
|
|
||||||
|
parseConfig :: String -> Maybe RemoteConfig
|
||||||
|
parseConfig = Just . keyValToConfig . words
|
||||||
|
|
||||||
|
showConfig :: RemoteConfig -> String
|
||||||
|
showConfig = unwords . configToKeyVal
|
||||||
|
|
||||||
|
{- Given Strings like "key=value", generates a RemoteConfig. -}
|
||||||
|
keyValToConfig :: [String] -> RemoteConfig
|
||||||
|
keyValToConfig ws = M.fromList $ map (/=/) ws
|
||||||
|
where
|
||||||
|
(/=/) s = (k, v)
|
||||||
|
where
|
||||||
|
k = takeWhile (/= '=') s
|
||||||
|
v = configUnEscape $ drop (1 + length k) s
|
||||||
|
|
||||||
|
configToKeyVal :: M.Map String String -> [String]
|
||||||
|
configToKeyVal m = map toword $ sort $ M.toList m
|
||||||
|
where
|
||||||
|
toword (k, v) = k ++ "=" ++ configEscape v
|
||||||
|
|
||||||
|
configEscape :: String -> String
|
||||||
|
configEscape = (>>= escape)
|
||||||
|
where
|
||||||
|
escape c
|
||||||
|
| isSpace c || c `elem` "&" = "&" ++ show (ord c) ++ ";"
|
||||||
|
| otherwise = [c]
|
||||||
|
|
||||||
|
configUnEscape :: String -> String
|
||||||
|
configUnEscape = unescape
|
||||||
|
where
|
||||||
|
unescape [] = []
|
||||||
|
unescape (c:rest)
|
||||||
|
| c == '&' = entity rest
|
||||||
|
| otherwise = c : unescape rest
|
||||||
|
entity s = if ok
|
||||||
|
then chr (Prelude.read num) : unescape rest
|
||||||
|
else '&' : unescape s
|
||||||
|
where
|
||||||
|
num = takeWhile isNumber s
|
||||||
|
r = drop (length num) s
|
||||||
|
rest = drop 1 r
|
||||||
|
ok = not (null num) && take 1 r == ";"
|
||||||
|
|
||||||
|
{- for quickcheck -}
|
||||||
|
prop_idempotent_configEscape :: String -> Bool
|
||||||
|
prop_idempotent_configEscape s = s == (configUnEscape . configEscape) s
|
85
Logs/Trust.hs
Normal file
85
Logs/Trust.hs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
{- git-annex trust
|
||||||
|
-
|
||||||
|
- Copyright 2010 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Logs.Trust (
|
||||||
|
TrustLevel(..),
|
||||||
|
trustGet,
|
||||||
|
trustSet,
|
||||||
|
trustPartition
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import Data.Time.Clock.POSIX
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import Types.TrustLevel
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import qualified Annex
|
||||||
|
import Logs.UUIDBased
|
||||||
|
|
||||||
|
{- Filename of trust.log. -}
|
||||||
|
trustLog :: FilePath
|
||||||
|
trustLog = "trust.log"
|
||||||
|
|
||||||
|
{- Returns a list of UUIDs that the trustLog indicates have the
|
||||||
|
- specified trust level.
|
||||||
|
- Note that the list can be incomplete for SemiTrusted, since that's
|
||||||
|
- the default. -}
|
||||||
|
trustGet :: TrustLevel -> Annex [UUID]
|
||||||
|
trustGet level = M.keys . M.filter (== level) <$> trustMap
|
||||||
|
|
||||||
|
{- Partitions a list of UUIDs to those matching a TrustLevel and not. -}
|
||||||
|
trustPartition :: TrustLevel -> [UUID] -> Annex ([UUID], [UUID])
|
||||||
|
trustPartition level ls
|
||||||
|
| level == SemiTrusted = do
|
||||||
|
t <- trustGet Trusted
|
||||||
|
u <- trustGet UnTrusted
|
||||||
|
d <- trustGet DeadTrusted
|
||||||
|
let uncandidates = t ++ u ++ d
|
||||||
|
return $ partition (`notElem` uncandidates) ls
|
||||||
|
| otherwise = do
|
||||||
|
candidates <- trustGet level
|
||||||
|
return $ partition (`elem` candidates) ls
|
||||||
|
|
||||||
|
{- Read the trustLog into a map, overriding with any
|
||||||
|
- values from forcetrust. The map is cached for speed. -}
|
||||||
|
trustMap :: Annex TrustMap
|
||||||
|
trustMap = do
|
||||||
|
cached <- Annex.getState Annex.trustmap
|
||||||
|
case cached of
|
||||||
|
Just m -> return m
|
||||||
|
Nothing -> do
|
||||||
|
overrides <- M.fromList <$> Annex.getState Annex.forcetrust
|
||||||
|
m <- (M.union overrides . simpleMap . parseLog (Just . parseTrust)) <$>
|
||||||
|
Annex.Branch.get trustLog
|
||||||
|
Annex.changeState $ \s -> s { Annex.trustmap = Just m }
|
||||||
|
return m
|
||||||
|
|
||||||
|
{- The trust.log used to only list trusted repos, without a field for the
|
||||||
|
- trust status, which is why this defaults to Trusted. -}
|
||||||
|
parseTrust :: String -> TrustLevel
|
||||||
|
parseTrust s = maybe Trusted parse $ headMaybe $ words s
|
||||||
|
where
|
||||||
|
parse "1" = Trusted
|
||||||
|
parse "0" = UnTrusted
|
||||||
|
parse "X" = DeadTrusted
|
||||||
|
parse _ = SemiTrusted
|
||||||
|
|
||||||
|
showTrust :: TrustLevel -> String
|
||||||
|
showTrust Trusted = "1"
|
||||||
|
showTrust UnTrusted = "0"
|
||||||
|
showTrust DeadTrusted = "X"
|
||||||
|
showTrust SemiTrusted = "?"
|
||||||
|
|
||||||
|
{- Changes the trust level for a uuid in the trustLog. -}
|
||||||
|
trustSet :: UUID -> TrustLevel -> Annex ()
|
||||||
|
trustSet uuid@(UUID _) level = do
|
||||||
|
ts <- liftIO getPOSIXTime
|
||||||
|
Annex.Branch.change trustLog $
|
||||||
|
showLog showTrust . changeLog ts uuid level . parseLog (Just . parseTrust)
|
||||||
|
Annex.changeState $ \s -> s { Annex.trustmap = Nothing }
|
||||||
|
trustSet NoUUID _ = error "unknown UUID; cannot modify trust level"
|
89
Logs/UUID.hs
Normal file
89
Logs/UUID.hs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
{- git-annex uuids
|
||||||
|
-
|
||||||
|
- Each git repository used by git-annex has an annex.uuid setting that
|
||||||
|
- uniquely identifies that repository.
|
||||||
|
-
|
||||||
|
- UUIDs of remotes are cached in git config, using keys named
|
||||||
|
- remote.<name>.annex-uuid
|
||||||
|
-
|
||||||
|
- uuid.log stores a list of known uuids, and their descriptions.
|
||||||
|
-
|
||||||
|
- Copyright 2010-2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Logs.UUID (
|
||||||
|
describeUUID,
|
||||||
|
recordUUID,
|
||||||
|
uuidMap
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import Data.Time.Clock.POSIX
|
||||||
|
|
||||||
|
import Common.Annex
|
||||||
|
import qualified Annex.Branch
|
||||||
|
import Logs.UUIDBased
|
||||||
|
import qualified Annex.UUID
|
||||||
|
|
||||||
|
{- Filename of uuid.log. -}
|
||||||
|
logfile :: FilePath
|
||||||
|
logfile = "uuid.log"
|
||||||
|
|
||||||
|
{- Records a description for a uuid in the log. -}
|
||||||
|
describeUUID :: UUID -> String -> Annex ()
|
||||||
|
describeUUID uuid desc = do
|
||||||
|
ts <- liftIO getPOSIXTime
|
||||||
|
Annex.Branch.change logfile $
|
||||||
|
showLog id . changeLog ts uuid desc . fixBadUUID . parseLog Just
|
||||||
|
|
||||||
|
{- Temporarily here to fix badly formatted uuid logs generated by
|
||||||
|
- versions 3.20111105 and 3.20111025.
|
||||||
|
-
|
||||||
|
- Those logs contain entries with the UUID and description flipped.
|
||||||
|
- Due to parsing, if the description is multiword, only the first
|
||||||
|
- will be taken to be the UUID. So, if the UUID of an entry does
|
||||||
|
- not look like a UUID, and the last word of the description does,
|
||||||
|
- flip them back.
|
||||||
|
-}
|
||||||
|
fixBadUUID :: Log String -> Log String
|
||||||
|
fixBadUUID = M.fromList . map fixup . M.toList
|
||||||
|
where
|
||||||
|
fixup (k, v)
|
||||||
|
| isbad = (fixeduuid, LogEntry (Date $ newertime v) fixedvalue)
|
||||||
|
| otherwise = (k, v)
|
||||||
|
where
|
||||||
|
kuuid = fromUUID k
|
||||||
|
isbad = not (isuuid kuuid) && isuuid lastword
|
||||||
|
ws = words $ value v
|
||||||
|
lastword = Prelude.last ws
|
||||||
|
fixeduuid = toUUID lastword
|
||||||
|
fixedvalue = unwords $ kuuid: Prelude.init ws
|
||||||
|
-- For the fixed line to take precidence, it should be
|
||||||
|
-- slightly newer, but only slightly.
|
||||||
|
newertime (LogEntry (Date d) _) = d + minimumPOSIXTimeSlice
|
||||||
|
newertime (LogEntry Unknown _) = minimumPOSIXTimeSlice
|
||||||
|
minimumPOSIXTimeSlice = 0.000001
|
||||||
|
isuuid s = length s == 36 && length (split "-" s) == 5
|
||||||
|
|
||||||
|
{- Records the uuid in the log, if it's not already there. -}
|
||||||
|
recordUUID :: UUID -> Annex ()
|
||||||
|
recordUUID u = go . M.lookup u =<< uuidMap
|
||||||
|
where
|
||||||
|
go (Just "") = set
|
||||||
|
go Nothing = set
|
||||||
|
go _ = return ()
|
||||||
|
set = describeUUID u ""
|
||||||
|
|
||||||
|
{- Read the uuidLog into a simple Map.
|
||||||
|
-
|
||||||
|
- The UUID of the current repository is included explicitly, since
|
||||||
|
- it may not have been described and so otherwise would not appear. -}
|
||||||
|
uuidMap :: Annex (M.Map UUID String)
|
||||||
|
uuidMap = do
|
||||||
|
m <- (simpleMap . parseLog Just) <$> Annex.Branch.get logfile
|
||||||
|
u <- Annex.UUID.getUUID
|
||||||
|
return $ M.insertWith' preferold u "" m
|
||||||
|
where
|
||||||
|
preferold = flip const
|
110
Logs/UUIDBased.hs
Normal file
110
Logs/UUIDBased.hs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
{- git-annex uuid-based logs
|
||||||
|
-
|
||||||
|
- This is used to store information about a UUID in a way that can
|
||||||
|
- be union merged.
|
||||||
|
-
|
||||||
|
- A line of the log will look like: "UUID[ INFO[ timestamp=foo]]"
|
||||||
|
- The timestamp is last for backwards compatability reasons,
|
||||||
|
- and may not be present on old log lines.
|
||||||
|
-
|
||||||
|
- Copyright 2011 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Logs.UUIDBased (
|
||||||
|
Log,
|
||||||
|
LogEntry(..),
|
||||||
|
TimeStamp(..),
|
||||||
|
parseLog,
|
||||||
|
showLog,
|
||||||
|
changeLog,
|
||||||
|
addLog,
|
||||||
|
simpleMap,
|
||||||
|
|
||||||
|
prop_TimeStamp_sane,
|
||||||
|
prop_addLog_sane,
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import Data.Time.Clock.POSIX
|
||||||
|
import Data.Time
|
||||||
|
import System.Locale
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Types.UUID
|
||||||
|
|
||||||
|
data TimeStamp = Unknown | Date POSIXTime
|
||||||
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
|
data LogEntry a = LogEntry
|
||||||
|
{ changed :: TimeStamp
|
||||||
|
, value :: a
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
type Log a = M.Map UUID (LogEntry a)
|
||||||
|
|
||||||
|
tskey :: String
|
||||||
|
tskey = "timestamp="
|
||||||
|
|
||||||
|
showLog :: (a -> String) -> Log a -> String
|
||||||
|
showLog shower = unlines . map showpair . M.toList
|
||||||
|
where
|
||||||
|
showpair (k, LogEntry (Date p) v) =
|
||||||
|
unwords [fromUUID k, shower v, tskey ++ show p]
|
||||||
|
showpair (k, LogEntry Unknown v) =
|
||||||
|
unwords [fromUUID k, shower v]
|
||||||
|
|
||||||
|
parseLog :: (String -> Maybe a) -> String -> Log a
|
||||||
|
parseLog parser = M.fromListWith best . mapMaybe parse . lines
|
||||||
|
where
|
||||||
|
parse line
|
||||||
|
| null ws = Nothing
|
||||||
|
| otherwise = parser (unwords info) >>= makepair
|
||||||
|
where
|
||||||
|
makepair v = Just (toUUID u, LogEntry ts v)
|
||||||
|
ws = words line
|
||||||
|
u = Prelude.head ws
|
||||||
|
t = Prelude.last ws
|
||||||
|
ts
|
||||||
|
| tskey `isPrefixOf` t =
|
||||||
|
pdate $ drop 1 $ dropWhile (/= '=') t
|
||||||
|
| otherwise = Unknown
|
||||||
|
info
|
||||||
|
| ts == Unknown = drop 1 ws
|
||||||
|
| otherwise = drop 1 $ beginning ws
|
||||||
|
pdate s = case parseTime defaultTimeLocale "%s%Qs" s of
|
||||||
|
Nothing -> Unknown
|
||||||
|
Just d -> Date $ utcTimeToPOSIXSeconds d
|
||||||
|
|
||||||
|
changeLog :: POSIXTime -> UUID -> a -> Log a -> Log a
|
||||||
|
changeLog t u v = M.insert u $ LogEntry (Date t) v
|
||||||
|
|
||||||
|
{- Only add an LogEntry if it's newer (or at least as new as) than any
|
||||||
|
- existing LogEntry for a UUID. -}
|
||||||
|
addLog :: UUID -> LogEntry a -> Log a -> Log a
|
||||||
|
addLog = M.insertWith best
|
||||||
|
|
||||||
|
{- Converts a Log into a simple Map without the timestamp information.
|
||||||
|
- This is a one-way trip, but useful for code that never needs to change
|
||||||
|
- the log. -}
|
||||||
|
simpleMap :: Log a -> M.Map UUID a
|
||||||
|
simpleMap = M.map value
|
||||||
|
|
||||||
|
best :: LogEntry a -> LogEntry a -> LogEntry a
|
||||||
|
best new old
|
||||||
|
| changed old > changed new = old
|
||||||
|
| otherwise = new
|
||||||
|
|
||||||
|
-- Unknown is oldest.
|
||||||
|
prop_TimeStamp_sane :: Bool
|
||||||
|
prop_TimeStamp_sane = Unknown < Date 1
|
||||||
|
|
||||||
|
prop_addLog_sane :: Bool
|
||||||
|
prop_addLog_sane = newWins && newestWins
|
||||||
|
where
|
||||||
|
newWins = addLog (UUID "foo") (LogEntry (Date 1) "new") l == l2
|
||||||
|
newestWins = addLog (UUID "foo") (LogEntry (Date 1) "newest") l2 /= l2
|
||||||
|
|
||||||
|
l = M.fromList [(UUID "foo", LogEntry (Date 0) "old")]
|
||||||
|
l2 = M.fromList [(UUID "foo", LogEntry (Date 1) "new")]
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue