diff --git a/Annex.hs b/Annex.hs
--- a/Annex.hs
+++ b/Annex.hs
@@ -58,6 +58,7 @@
 import Types.FileMatcher
 import Types.NumCopies
 import Types.LockPool
+import Types.MetaData
 import qualified Utility.Matcher
 import qualified Data.Map as M
 import qualified Data.Set as S
@@ -109,6 +110,7 @@
 	, lockpool :: LockPool
 	, flags :: M.Map String Bool
 	, fields :: M.Map String String
+	, modmeta :: [ModMeta]
 	, cleanup :: M.Map String (Annex ())
 	, inodeschanged :: Maybe Bool
 	, useragent :: Maybe String
@@ -146,6 +148,7 @@
 	, lockpool = M.empty
 	, flags = M.empty
 	, fields = M.empty
+	, modmeta = []
 	, cleanup = M.empty
 	, inodeschanged = Nothing
 	, useragent = Nothing
diff --git a/Annex/Branch.hs b/Annex/Branch.hs
--- a/Annex/Branch.hs
+++ b/Annex/Branch.hs
@@ -5,8 +5,6 @@
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
-{-# LANGUAGE CPP #-}
-
 module Annex.Branch (
 	fullname,
 	name,
@@ -30,11 +28,11 @@
 import qualified Data.ByteString.Lazy.Char8 as L
 import qualified Data.Set as S
 import qualified Data.Map as M
-import qualified Control.Exception as E
 
 import Common.Annex
 import Annex.BranchState
 import Annex.Journal
+import Annex.Index
 import qualified Git
 import qualified Git.Command
 import qualified Git.Ref
@@ -47,15 +45,12 @@
 import Git.FilePath
 import Annex.CatFile
 import Annex.Perms
-import qualified Annex
-import Utility.Env
 import Logs
 import Logs.Transitions
 import Logs.Trust.Pure
 import Annex.ReplaceFile
 import qualified Annex.Queue
 import Annex.Branch.Transitions
-import Annex.Exception
 
 {- Name of the branch that is used to store git-annex's information. -}
 name :: Git.Ref
@@ -63,11 +58,11 @@
 
 {- Fully qualified name of the branch. -}
 fullname :: Git.Ref
-fullname = Git.Ref $ "refs/heads/" ++ show name
+fullname = Git.Ref $ "refs/heads/" ++ fromRef name
 
 {- Branch's name in origin. -}
 originname :: Git.Ref
-originname = Git.Ref $ "origin/" ++ show name
+originname = Git.Ref $ "origin/" ++ fromRef name
 
 {- Does origin/git-annex exist? -}
 hasOrigin :: Annex Bool
@@ -92,8 +87,8 @@
   where
 	go True = do
 		inRepo $ Git.Command.run
-			[Param "branch", Param $ show name, Param $ show originname]
-		fromMaybe (error $ "failed to create " ++ show name)
+			[Param "branch", Param $ fromRef name, Param $ fromRef originname]
+		fromMaybe (error $ "failed to create " ++ fromRef name)
 			<$> branchsha
 	go False = withIndex' True $
 		inRepo $ Git.Branch.commitAlways "branch created" fullname []
@@ -159,7 +154,7 @@
 			then "update"
 			else "merging " ++
 				unwords (map Git.Ref.describe branches) ++ 
-				" into " ++ show name
+				" into " ++ fromRef name
 		localtransitions <- parseTransitionsStrictly "local"
 			<$> getLocal transitionsLog
 		unless (null branches) $ do
@@ -296,7 +291,7 @@
 branchFiles :: Annex [FilePath]
 branchFiles = withIndex $ inRepo $ Git.Command.pipeNullSplitZombie
 	[ Params "ls-tree --name-only -r -z"
-	, Param $ show fullname
+	, Param $ fromRef fullname
 	]
 
 {- Populates the branch's index file with the current branch contents.
@@ -338,32 +333,12 @@
 withIndex' :: Bool -> Annex a -> Annex a
 withIndex' bootstrapping a = do
 	f <- fromRepo gitAnnexIndex
-	g <- gitRepo
-#ifdef __ANDROID__
-	{- This should not be necessary on Android, but there is some
-	 - weird getEnvironment breakage. See
-	 - https://github.com/neurocyte/ghc-android/issues/7
-	 - Use getEnv to get some key environment variables that
-	 - git expects to have. -}
-	let keyenv = words "USER PATH GIT_EXEC_PATH HOSTNAME HOME"
-	let getEnvPair k = maybe Nothing (\v -> Just (k, v)) <$> getEnv k
-	e <- liftIO $ catMaybes <$> forM keyenv getEnvPair
-	let e' = ("GIT_INDEX_FILE", f):e
-#else
-	e <- liftIO getEnvironment
-	let e' = addEntry "GIT_INDEX_FILE" f e
-#endif
-	let g' = g { gitEnv = Just e' }
-
-	r <- tryAnnex $ do
-		Annex.changeState $ \s -> s { Annex.repo = g' }
+	withIndexFile f $ do
 		checkIndexOnce $ unlessM (liftIO $ doesFileExist f) $ do
 			unless bootstrapping create
 			createAnnexDirectory $ takeDirectory f
 			unless bootstrapping $ inRepo genIndex
 		a
-	Annex.changeState $ \s -> s { Annex.repo = (Annex.repo s) { gitEnv = gitEnv g} }
-	either E.throw return r
 
 {- Updates the branch's index to reflect the current contents of the branch.
  - Any changes staged in the index will be preserved.
@@ -393,7 +368,7 @@
 setIndexSha :: Git.Ref -> Annex ()
 setIndexSha ref = do
 	f <- fromRepo gitAnnexIndexStatus
-	liftIO $ writeFile f $ show ref ++ "\n"
+	liftIO $ writeFile f $ fromRef ref ++ "\n"
 	setAnnexFilePerm f
 
 {- Stages the journal into the index and returns an action that will
@@ -467,7 +442,7 @@
 	let s = S.unions [old, S.fromList rs]
 	f <- fromRepo gitAnnexIgnoredRefs
 	replaceFile f $ \tmp -> liftIO $ writeFile tmp $
-		unlines $ map show $ S.elems s
+		unlines $ map fromRef $ S.elems s
 
 getIgnoredRefs :: Annex (S.Set Git.Ref)
 getIgnoredRefs = S.fromList . mapMaybe Git.Sha.extractSha . lines <$> content
diff --git a/Annex/Branch/Transitions.hs b/Annex/Branch/Transitions.hs
--- a/Annex/Branch/Transitions.hs
+++ b/Annex/Branch/Transitions.hs
@@ -41,7 +41,7 @@
 		in if null newlog
 			then RemoveFile
 			else ChangeFile $ Presence.showLog newlog
-	Just SingleValueLog -> PreserveFile
+	Just OtherLog -> PreserveFile
 	Nothing -> PreserveFile
 
 dropDeadFromUUIDBasedLog :: TrustMap -> UUIDBased.Log String -> UUIDBased.Log String
diff --git a/Annex/Content.hs b/Annex/Content.hs
--- a/Annex/Content.hs
+++ b/Annex/Content.hs
@@ -243,10 +243,9 @@
 			moveAnnex key tmpfile
 			logStatus key InfoPresent
 			return True
-		, do
-			-- the tmp file is left behind, in case caller wants
-			-- to resume its transfer
-			return False
+		-- the tmp file is left behind, in case caller wants
+		-- to resume its transfer
+		, return False
 		)
 
 prepTmp :: Key -> Annex FilePath
@@ -492,9 +491,11 @@
 
 	{- In indirect mode, look for the key. In direct mode,
 	 - the inode cache file is only present when a key's content
-	 - is present. -}
+	 - is present, so can be used as a surrogate if the content
+	 - is not located in the annex directory. -}
 	present False d = doesFileExist $ contentfile d
-	present True d = doesFileExist $ contentfile d ++ ".cache"
+	present True d = doesFileExist (contentfile d ++ ".cache")
+		<||> present False d
 	contentfile d = d </> takeFileName d
 
 {- Things to do to record changes to content when shutting down.
diff --git a/Annex/Content/Direct.hs b/Annex/Content/Direct.hs
--- a/Annex/Content/Direct.hs
+++ b/Annex/Content/Direct.hs
@@ -66,7 +66,7 @@
 	mapping <- calcRepo $ gitAnnexMapping key
 	files <- associatedFilesRelative key
 	let files' = transform files
-	when (files /= files') $ do
+	when (files /= files') $
 		modifyContent mapping $
 			liftIO $ viaTmp writeFileAnyEncoding mapping $
 				unlines files'
diff --git a/Annex/Direct.hs b/Annex/Direct.hs
--- a/Annex/Direct.hs
+++ b/Annex/Direct.hs
@@ -184,7 +184,7 @@
 			tryAnnex . maybe (araw f item) (\k -> void $ a k f)
 				=<< catKey (getsha item) (getmode item)
 
-	moveout k f = removeDirect k f
+	moveout = removeDirect
 
 	{- Files deleted by the merge are removed from the work tree.
 	 - Empty work tree directories are removed, per git behavior. -}
@@ -286,18 +286,18 @@
  - this way things that show HEAD (eg shell prompts) will
  - hopefully show just "master". -}
 directBranch :: Ref -> Ref
-directBranch orighead = case split "/" $ show orighead of
+directBranch orighead = case split "/" $ fromRef orighead of
 	("refs":"heads":"annex":"direct":_) -> orighead
 	("refs":"heads":rest) ->
 		Ref $ "refs/heads/annex/direct/" ++ intercalate "/" rest
-	_ -> Ref $ "refs/heads/" ++ show (Git.Ref.base orighead)
+	_ -> Ref $ "refs/heads/" ++ fromRef (Git.Ref.base orighead)
 
 {- Converts a directBranch back to the original branch.
  -
  - Any other ref is left unchanged.
  -}
 fromDirectBranch :: Ref -> Ref
-fromDirectBranch directhead = case split "/" $ show directhead of
+fromDirectBranch directhead = case split "/" $ fromRef directhead of
 	("refs":"heads":"annex":"direct":rest) -> 
 		Ref $ "refs/heads/" ++ intercalate "/" rest
 	_ -> directhead
diff --git a/Annex/FileMatcher.hs b/Annex/FileMatcher.hs
--- a/Annex/FileMatcher.hs
+++ b/Annex/FileMatcher.hs
@@ -43,7 +43,7 @@
 fileMatchInfo :: FilePath -> Annex MatchInfo
 fileMatchInfo file = do
 	matchfile <- getTopFilePath <$> inRepo (toTopFilePath file)
-	return $ MatchingFile $ FileInfo
+	return $ MatchingFile FileInfo
 		{ matchFile = matchfile
 		, relFile = file
 		}
@@ -83,6 +83,7 @@
 			, ("inbackend", limitInBackend)
 			, ("largerthan", limitSize (>))
 			, ("smallerthan", limitSize (<))
+			, ("metadata", limitMetaData)
 			, ("inallgroup", limitInAllGroup groupmap)
 			]
   where
diff --git a/Annex/Index.hs b/Annex/Index.hs
new file mode 100644
--- /dev/null
+++ b/Annex/Index.hs
@@ -0,0 +1,46 @@
+{- Using other git index files
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE CPP #-}
+
+module Annex.Index (
+	withIndexFile,
+) where
+
+import qualified Control.Exception as E
+
+import Common.Annex
+import Git.Types
+import qualified Annex
+import Utility.Env
+import Annex.Exception
+
+{- Runs an action using a different git index file. -}
+withIndexFile :: FilePath -> Annex a -> Annex a
+withIndexFile f a = do
+	g <- gitRepo
+#ifdef __ANDROID__
+	{- This should not be necessary on Android, but there is some
+	 - weird getEnvironment breakage. See
+	 - https://github.com/neurocyte/ghc-android/issues/7
+	 - Use getEnv to get some key environment variables that
+	 - git expects to have. -}
+	let keyenv = words "USER PATH GIT_EXEC_PATH HOSTNAME HOME"
+	let getEnvPair k = maybe Nothing (\v -> Just (k, v)) <$> getEnv k
+	e <- liftIO $ catMaybes <$> forM keyenv getEnvPair
+	let e' = ("GIT_INDEX_FILE", f):e
+#else
+	e <- liftIO getEnvironment
+	let e' = addEntry "GIT_INDEX_FILE" f e
+#endif
+	let g' = g { gitEnv = Just e' }
+
+	r <- tryAnnex $ do
+		Annex.changeState $ \s -> s { Annex.repo = g' }
+		a
+	Annex.changeState $ \s -> s { Annex.repo = (Annex.repo s) { gitEnv = gitEnv g} }
+	either E.throw return r
diff --git a/Annex/Init.hs b/Annex/Init.hs
--- a/Annex/Init.hs
+++ b/Annex/Init.hs
@@ -70,11 +70,10 @@
 		( do
 			enableDirectMode
 			setDirect True
-		, do
-			-- Handle case where this repo was cloned from a
-			-- direct mode repo.
-			unlessM isBare
-				switchHEADBack
+		-- Handle case where this repo was cloned from a
+		-- direct mode repo
+		, unlessM isBare
+			switchHEADBack
 		)
 	createInodeSentinalFile
 	u <- getUUID
@@ -227,7 +226,7 @@
 		logStatus k InfoPresent
 	let dotgit = d </> ".git"
 	liftIO $ removeDirectoryRecursive dotgit
-		`catchIO` (const $ renameDirectory dotgit (d </> "removeme"))
+		`catchIO` const (renameDirectory dotgit (d </> "removeme"))
 
 {- A repostory with the problem won't know it's a bare repository, but will
  - have no pre-commit hook (which is not set up in a bare repository),
diff --git a/Annex/Link.hs b/Annex/Link.hs
--- a/Annex/Link.hs
+++ b/Annex/Link.hs
@@ -94,6 +94,10 @@
 hashSymlink linktarget = inRepo $ Git.HashObject.hashObject BlobObject $ 
 	toInternalGitPath linktarget
 
+hashSymlink' :: Git.HashObject.HashObjectHandle -> LinkTarget -> Annex Sha
+hashSymlink' h linktarget = liftIO $ Git.HashObject.hashBlob h $
+	toInternalGitPath linktarget
+
 {- Stages a symlink to the annex, using a Sha of its target. -}
 stageSymlink :: FilePath -> Sha -> Annex ()
 stageSymlink file sha =
diff --git a/Annex/Ssh.hs b/Annex/Ssh.hs
--- a/Annex/Ssh.hs
+++ b/Annex/Ssh.hs
@@ -76,7 +76,7 @@
   	-- ssh appends a 16 char extension to the socket when setting it
 	-- up, which needs to be taken into account when checking
 	-- that a valid socket was constructed.
-  	sshgarbage = take (1+16) $ repeat 'X'
+  	sshgarbage = replicate (1+16) 'X'
 
 sshConnectionCachingParams :: FilePath -> [CommandParam]
 sshConnectionCachingParams socketfile = 
diff --git a/Annex/TaggedPush.hs b/Annex/TaggedPush.hs
--- a/Annex/TaggedPush.hs
+++ b/Annex/TaggedPush.hs
@@ -35,11 +35,11 @@
 	[ Just "refs/synced"
 	, Just $ fromUUID u
 	, toB64 <$> info
-	, Just $ show $ Git.Ref.base b
+	, Just $ Git.fromRef $ Git.Ref.base b
 	]
 
 fromTaggedBranch :: Git.Branch -> Maybe (UUID, Maybe String)
-fromTaggedBranch b = case split "/" $ show b of
+fromTaggedBranch b = case split "/" $ Git.fromRef b of
 	("refs":"synced":u:info:_base) ->
 		Just (toUUID u, fromB64Maybe info)
 	("refs":"synced":u:_base) ->
@@ -58,4 +58,4 @@
         , Param $ refspec branch
         ]
   where
-	refspec b = show b ++ ":" ++ show (toTaggedBranch u info b)
+	refspec b = Git.fromRef b ++ ":" ++ Git.fromRef (toTaggedBranch u info b)
diff --git a/Annex/View.hs b/Annex/View.hs
new file mode 100644
--- /dev/null
+++ b/Annex/View.hs
@@ -0,0 +1,429 @@
+{- metadata based branch views
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE CPP #-}
+
+module Annex.View where
+
+import Common.Annex
+import Types.View
+import Types.MetaData
+import qualified Git
+import qualified Git.DiffTree as DiffTree
+import qualified Git.Branch
+import qualified Git.LsFiles
+import qualified Git.Ref
+import Git.UpdateIndex
+import Git.Sha
+import Git.HashObject
+import Git.Types
+import Git.FilePath
+import qualified Backend
+import Annex.Index
+import Annex.Link
+import Annex.CatFile
+import Logs.MetaData
+import Logs.View
+import Utility.FileMode
+import Types.Command
+import Config
+import CmdLine.Action
+
+import qualified Data.Set as S
+import System.Path.WildMatch
+import "mtl" Control.Monad.Writer
+
+#ifdef WITH_TDFA
+import Text.Regex.TDFA
+import Text.Regex.TDFA.String
+#else
+import Text.Regex
+#endif
+
+{- Each visible ViewFilter in a view results in another level of
+ - subdirectory nesting. When a file matches multiple ways, it will appear
+ - in multiple subdirectories. This means there is a bit of an exponential
+ - blowup with a single file appearing in a crazy number of places!
+ -
+ - Capping the view size to 5 is reasonable; why wants to dig
+ - through 5+ levels of subdirectories to find anything?
+ -}
+viewTooLarge :: View -> Bool
+viewTooLarge view = visibleViewSize view > 5
+
+visibleViewSize :: View -> Int
+visibleViewSize = length . filter viewVisible . viewComponents
+
+data ViewChange = Unchanged | Narrowing | Widening
+	deriving (Ord, Eq, Show)
+
+{- Updates a view, adding new fields to filter on (Narrowing), 
+ - or allowing new values in an existing field (Widening). -}
+refineView :: View -> [(MetaField, String)] -> (View, ViewChange)
+refineView = go Unchanged
+  where
+  	go c v [] = (v, c)
+	go c v ((f, s):rest) =
+		let (v', c') = refineView' v f s
+		in go (max c c') v' rest
+
+{- Adds an additional filter to a view. This can only result in narrowing
+ - the view. Multivalued filters are added in non-visible form. -}
+filterView :: View -> [(MetaField, String)] -> View
+filterView v vs = v { viewComponents = viewComponents f' ++ viewComponents v}
+  where
+	f = fst $ refineView (v {viewComponents = []}) vs
+	f' = f { viewComponents = map toinvisible (viewComponents f) }
+	toinvisible c = c { viewVisible = False }
+
+refineView' :: View -> MetaField -> String -> (View, ViewChange)
+refineView' view field wanted
+	| field `elem` (map viewField components) = 
+		let (components', viewchanges) = runWriter $ mapM updatefield components
+		in (view { viewComponents = components' }, maximum viewchanges)
+	| otherwise = 
+		let component = ViewComponent field viewfilter (multiValue viewfilter)
+		    view' = view { viewComponents = component : components }
+		in if viewTooLarge view'
+			then error $ "View is too large (" ++ show (visibleViewSize view') ++ " levels of subdirectories)"
+			else (view', Narrowing)
+  where
+  	components = viewComponents view
+	viewfilter
+		| any (`elem` wanted) "*?" = FilterGlob wanted
+		| otherwise = FilterValues $ S.singleton $ toMetaValue wanted
+	updatefield :: ViewComponent -> Writer [ViewChange] ViewComponent
+	updatefield v
+		| viewField v == field = do
+			let (newvf, viewchange) = combineViewFilter (viewFilter v) viewfilter
+			tell [viewchange]
+			return $ v { viewFilter = newvf }
+		| otherwise = return v
+
+{- Combine old and new ViewFilters, yielding a result that matches
+ - either old+new, or only new.
+ -
+ - If we have FilterValues and change to a FilterGlob,
+ - it's always a widening change, because the glob could match other
+ - values. OTOH, going the other way, it's a Narrowing change if the old
+ - glob matches all the new FilterValues.
+ -
+ - With two globs, the old one is discarded, and the new one is used.
+ - We can tell if that's a narrowing change by checking if the old
+ - glob matches the new glob. For example, "*" matches "foo*",
+ - so that's narrowing. While "f?o" does not match "f??", so that's
+ - widening.
+ -}
+combineViewFilter :: ViewFilter -> ViewFilter -> (ViewFilter, ViewChange)
+combineViewFilter old@(FilterValues olds) (FilterValues news)
+	| combined == old = (combined, Unchanged)
+	| otherwise = (combined, Widening)
+  where
+	combined = FilterValues (S.union olds news)
+combineViewFilter (FilterValues _) newglob@(FilterGlob _) =
+	(newglob, Widening)
+combineViewFilter (FilterGlob oldglob) new@(FilterValues s)
+	| all (matchGlob (compileGlob oldglob) . fromMetaValue) (S.toList s) = (new, Narrowing)
+	| otherwise = (new, Widening)
+combineViewFilter (FilterGlob old) newglob@(FilterGlob new)
+	| old == new = (newglob, Unchanged)
+	| matchGlob (compileGlob old) new = (newglob, Narrowing)
+	| otherwise = (newglob, Widening)
+
+{- Converts a filepath used in a reference branch to the
+ - filename that will be used in the view.
+ -
+ - No two filepaths from the same branch should yeild the same result,
+ - so all directory structure needs to be included in the output file
+ - in some way. However, the branch's directory structure is not relevant
+ - in the view.
+ -
+ - So, from dir/subdir/file.foo, generate file_{dir;subdir}.foo
+ -
+ - (To avoid collisions with a filename that already contains {foo},
+ - that is doubled to {{foo}}.)
+ -}
+fileViewFromReference :: MkFileView
+fileViewFromReference f = concat
+	[ double base
+	, if null dirs then "" else "_{" ++ double (intercalate ";" dirs) ++ "}"
+	, double $ concat extensions
+	]
+  where
+	(path, basefile) = splitFileName f
+	dirs = filter (/= ".") $ map dropTrailingPathSeparator (splitPath path)
+	(base, extensions) = splitShortExtensions basefile
+
+	double = replace "{" "{{" . replace "}" "}}"
+
+fileViewReuse :: MkFileView
+fileViewReuse = takeFileName
+
+{- Generates views for a file from a branch, based on its metadata
+ - and the filename used in the branch.
+ -
+ - Note that a file may appear multiple times in a view, when it
+ - has multiple matching values for a MetaField used in the View.
+ -
+ - Of course if its MetaData does not match the View, it won't appear at
+ - all.
+ -
+ - Note that for efficiency, it's useful to partially
+ - evaluate this function with the view parameter and reuse
+ - the result. The globs in the view will then be compiled and memoized.
+ -}
+fileViews :: View -> MkFileView -> FilePath -> MetaData -> [FileView]
+fileViews view = 
+	let matchers = map viewComponentMatcher (viewComponents view)
+	in \mkfileview file metadata ->
+		let matches = map (\m -> m metadata) matchers
+		in if any isNothing matches
+			then []
+			else 
+				let paths = pathProduct $
+					map (map toViewPath) (visible matches)
+				in if null paths
+					then [mkfileview file]
+					else map (</> mkfileview file) paths
+  where
+	visible = map (fromJust . snd) .
+		filter (viewVisible . fst) .
+		zip (viewComponents view)
+
+{- Checks if metadata matches a ViewComponent filter, and if so
+ - returns the value, or values that match. Self-memoizing on ViewComponent. -}
+viewComponentMatcher :: ViewComponent -> (MetaData -> Maybe [MetaValue])
+viewComponentMatcher viewcomponent = \metadata -> 
+	let s = matcher (currentMetaDataValues metafield metadata)
+	in if S.null s then Nothing else Just (S.toList s)
+  where
+  	metafield = viewField viewcomponent
+	matcher = case viewFilter viewcomponent of
+		FilterValues s -> \values -> S.intersection s values
+		FilterGlob glob ->
+			let regex = compileGlob glob
+			in \values -> 
+				S.filter (matchGlob regex . fromMetaValue) values
+
+compileGlob :: String -> Regex
+compileGlob glob = 
+#ifdef WITH_TDFA
+	case compile (defaultCompOpt {caseSensitive = False}) defaultExecOpt regex of
+		Right r -> r
+		Left _ -> error $ "failed to compile regex: " ++ regex
+#else
+	mkRegexWithOpts regex False True
+#endif
+  where
+	regex = '^':wildToRegex glob
+
+matchGlob :: Regex -> String -> Bool
+matchGlob regex val = 
+#ifdef WITH_TDFA
+	case execute regex val of
+		Right (Just _) -> True
+		_ -> False
+#else
+	isJust $ matchRegex regex val
+#endif
+
+toViewPath :: MetaValue -> FilePath
+toViewPath = concatMap escapeslash . fromMetaValue
+  where
+	escapeslash c
+		| c == '/' = [pseudoSlash]
+		| c == '\\' = [pseudoBackslash]
+		| c == pseudoSlash = [pseudoSlash, pseudoSlash]
+		| c == pseudoBackslash = [pseudoBackslash, pseudoBackslash]
+		| otherwise = [c]
+
+fromViewPath :: FilePath -> MetaValue
+fromViewPath = toMetaValue . deescapeslash []
+  where
+  	deescapeslash s [] = reverse s
+  	deescapeslash s (c:cs)
+		| c == pseudoSlash = case cs of
+			(c':cs')
+				| c' == pseudoSlash -> deescapeslash (pseudoSlash:s) cs'
+			_ -> deescapeslash ('/':s) cs
+		| c == pseudoBackslash = case cs of
+			(c':cs')
+				| c' == pseudoBackslash -> deescapeslash (pseudoBackslash:s) cs'
+			_ -> deescapeslash ('/':s) cs
+		| otherwise = deescapeslash (c:s) cs
+
+pseudoSlash :: Char
+pseudoSlash = '\8725' -- '∕' /= '/'
+
+pseudoBackslash :: Char
+pseudoBackslash = '\9586' -- '╲' /= '\'
+
+pathProduct :: [[FilePath]] -> [FilePath]
+pathProduct [] = []
+pathProduct (l:ls) = foldl combinel l ls
+  where
+	combinel xs ys = [combine x y | x <- xs, y <- ys]
+
+{- Extracts the metadata from a fileview, based on the view that was used
+ - to construct it. -}
+fromView :: View -> FileView -> MetaData
+fromView view f = foldr (uncurry updateMetaData) newMetaData (zip fields values)
+  where
+	visible = filter viewVisible (viewComponents view)
+	fields = map viewField visible
+	paths = splitDirectories $ dropFileName f
+	values = map fromViewPath paths
+
+{- Constructing a view that will match arbitrary metadata, and applying
+ - it to a file yields a set of FileViews which all contain the same
+ - MetaFields that were present in the input metadata
+ - (excluding fields that are not visible). -}
+prop_view_roundtrips :: FilePath -> MetaData -> Bool -> Bool
+prop_view_roundtrips f metadata visible = null f || viewTooLarge view ||
+	all hasfields (fileViews view fileViewFromReference f metadata)
+  where
+	view = View (Git.Ref "master") $
+		map (\(mf, mv) -> ViewComponent mf (FilterValues $ S.filter (not . null . fromMetaValue) mv) visible)
+			(fromMetaData metadata)
+	visiblefields = sort (map viewField $ filter viewVisible (viewComponents view))
+	hasfields fv = sort (map fst (fromMetaData (fromView view fv))) == visiblefields
+
+{- Applies a view to the currently checked out branch, generating a new
+ - branch for the view.
+ -}
+applyView :: View -> Annex Git.Branch
+applyView view = applyView' fileViewFromReference view
+
+{- Generates a new branch for a View, which must be a more narrow
+ - version of the View originally used to generate the currently
+ - checked out branch. That is, it must match a subset of the files
+ - in view, not any others.
+ -}
+narrowView :: View -> Annex Git.Branch
+narrowView = applyView' fileViewReuse
+
+{- Go through each file in the currently checked out branch.
+ - If the file is not annexed, skip it, unless it's a dotfile in the top.
+ - Look up the metadata of annexed files, and generate any FileViews,
+ - and stage them.
+ -
+ - Currently only works in indirect mode. Must be run from top of
+ - repository.
+ -}
+applyView' :: MkFileView -> View -> Annex Git.Branch
+applyView' mkfileview view = do
+	top <- fromRepo Git.repoPath
+	(l, clean) <- inRepo $ Git.LsFiles.inRepo [top]
+	liftIO . nukeFile =<< fromRepo gitAnnexViewIndex
+	genViewBranch view $ do
+		uh <- inRepo Git.UpdateIndex.startUpdateIndex
+		hasher <- inRepo hashObjectStart
+		forM_ l $ \f ->
+			go uh hasher f =<< Backend.lookupFile f
+		liftIO $ do
+			hashObjectStop hasher
+			void $ stopUpdateIndex uh
+			void clean
+  where
+	genfileviews = fileViews view mkfileview -- enables memoization
+	go uh hasher f (Just (k, _)) = do
+		metadata <- getCurrentMetaData k
+		forM_ (genfileviews f metadata) $ \fv -> do
+			stagesymlink uh hasher fv =<< inRepo (gitAnnexLink fv k)
+	go uh hasher f Nothing
+		| "." `isPrefixOf` f = do
+			s <- liftIO $ getSymbolicLinkStatus f
+			if isSymbolicLink s
+				then stagesymlink uh hasher f =<< liftIO (readSymbolicLink f)
+				else do
+					sha <- liftIO $ Git.HashObject.hashFile hasher f
+					let blobtype = if isExecutable (fileMode s)
+						then ExecutableBlob
+						else FileBlob
+					liftIO . Git.UpdateIndex.streamUpdateIndex' uh
+						=<< inRepo (Git.UpdateIndex.stageFile sha blobtype f)
+		| otherwise = noop
+	stagesymlink uh hasher f linktarget = do
+		sha <- hashSymlink' hasher linktarget
+		liftIO . Git.UpdateIndex.streamUpdateIndex' uh
+			=<< inRepo (Git.UpdateIndex.stageSymlink f sha)
+
+{- Applies a view to the reference branch, generating a new branch
+ - for the View.
+ -
+ - This needs to work incrementally, to quickly update the view branch
+ - when the reference branch is changed. So, it works based on an
+ - old version of the reference branch, uses diffTree to find the
+ - changes, and applies those changes to the view branch.
+ -}
+updateView :: View -> Git.Ref -> Git.Ref -> Annex Git.Branch
+updateView view ref oldref = genViewBranch view $ do
+	(diffs, cleanup) <- inRepo $ DiffTree.diffTree oldref ref
+	forM_ diffs go
+	void $ liftIO cleanup
+  where
+	go diff
+		| DiffTree.dstsha diff == nullSha = error "TODO delete file"
+		| otherwise = error "TODO add file"
+
+{- Diff between currently checked out branch and staged changes, and
+ - update metadata to reflect the changes that are being committed to the
+ - view.
+ -
+ - Adding a file to a directory adds the metadata represented by
+ - that directory to the file, and removing a file from a directory
+ - removes the metadata.
+ -
+ - Note that removes must be handled before adds. This is so
+ - that moving a file from x/foo/ to x/bar/ adds back the metadata for x.
+ -}
+withViewChanges :: (FileView -> Key -> CommandStart) -> (FileView -> Key -> CommandStart) -> Annex ()
+withViewChanges addmeta removemeta = do
+	makeabs <- flip fromTopFilePath <$> gitRepo
+	(diffs, cleanup) <- inRepo $ DiffTree.diffIndex Git.Ref.headRef
+	forM_ diffs handleremovals
+	forM_ diffs (handleadds makeabs)
+	void $ liftIO cleanup
+  where
+	handleremovals item
+		| DiffTree.srcsha item /= nullSha =
+			handle item removemeta
+				=<< catKey (DiffTree.srcsha item) (DiffTree.srcmode item)
+		| otherwise = noop
+	handleadds makeabs item
+		| DiffTree.dstsha item /= nullSha = 
+			handle item addmeta
+				=<< ifM isDirect
+					( catKey (DiffTree.dstsha item) (DiffTree.dstmode item)
+					-- optimisation
+					, isAnnexLink $ makeabs $ DiffTree.file item
+					)
+		| otherwise = noop
+	handle item a = maybe noop
+		(void . commandAction . a (getTopFilePath $ DiffTree.file item))
+
+{- Generates a branch for a view. This is done using a different index
+ - file. An action is run to stage the files that will be in the branch.
+ - Then a commit is made, to the view branch. The view branch is not
+ - checked out, but entering it will display the view. -}
+genViewBranch :: View -> Annex () -> Annex Git.Branch
+genViewBranch view a = withIndex $ do
+	a
+	let branch = branchView view
+	void $ inRepo $ Git.Branch.commit True (fromRef branch) branch []
+	return branch
+
+{- Runs an action using the view index file.
+ - Note that the file does not necessarily exist, or can contain
+ - info staged for an old view. -}
+withIndex :: Annex a -> Annex a
+withIndex a = do
+	f <- fromRepo gitAnnexViewIndex
+	withIndexFile f a
+
+withCurrentView :: (View -> Annex a) -> Annex a
+withCurrentView a = maybe (error "Not in a view.") a =<< currentView
diff --git a/Assistant.hs b/Assistant.hs
--- a/Assistant.hs
+++ b/Assistant.hs
@@ -93,7 +93,8 @@
 			start (Utility.Daemon.daemonize logfd (Just pidfile) False) Nothing
 #else
 	-- Windows is always foreground, and has no log file.
-	start id $
+	liftIO $ Utility.Daemon.lockPidFile pidfile
+	start id $ do
 		case startbrowser of
 			Nothing -> Nothing
 			Just a -> Just $ a Nothing Nothing
diff --git a/Assistant/MakeRemote.hs b/Assistant/MakeRemote.hs
--- a/Assistant/MakeRemote.hs
+++ b/Assistant/MakeRemote.hs
@@ -48,9 +48,10 @@
 makeRsyncRemote name location = makeRemote name location $ const $ void $
 	go =<< Command.InitRemote.findExisting name
   where
-  	go Nothing = setupSpecialRemote name Rsync.remote config
+  	go Nothing = setupSpecialRemote name Rsync.remote config Nothing
 		(Nothing, Command.InitRemote.newConfig name)
-	go (Just (u, c)) = setupSpecialRemote name Rsync.remote config (Just u, c)
+	go (Just (u, c)) = setupSpecialRemote name Rsync.remote config Nothing
+		(Just u, c)
 	config = M.fromList
 		[ ("encryption", "shared")
 		, ("rsyncurl", location)
@@ -60,44 +61,44 @@
 {- Inits a gcrypt special remote, and returns its name. -}
 makeGCryptRemote :: RemoteName -> String -> KeyId -> Annex RemoteName
 makeGCryptRemote remotename location keyid = 
-	initSpecialRemote remotename GCrypt.remote $ M.fromList
+	initSpecialRemote remotename GCrypt.remote Nothing $ M.fromList
 		[ ("type", "gcrypt")
 		, ("gitrepo", location)
 		, configureEncryption HybridEncryption
 		, ("keyid", keyid)
 		]
 
-type SpecialRemoteMaker = RemoteName -> RemoteType -> R.RemoteConfig -> Annex RemoteName
+type SpecialRemoteMaker = RemoteName -> RemoteType -> Maybe CredPair -> R.RemoteConfig -> Annex RemoteName
 
 {- Inits a new special remote. The name is used as a suggestion, but
  - will be changed if there is already a special remote with that name. -}
 initSpecialRemote :: SpecialRemoteMaker
-initSpecialRemote name remotetype config = go 0
+initSpecialRemote name remotetype mcreds config = go 0
   where
 	go :: Int -> Annex RemoteName
 	go n = do
 		let fullname = if n == 0  then name else name ++ show n
 		r <- Command.InitRemote.findExisting fullname
 		case r of
-			Nothing -> setupSpecialRemote fullname remotetype config
+			Nothing -> setupSpecialRemote fullname remotetype config mcreds
 				(Nothing, Command.InitRemote.newConfig fullname)
 			Just _ -> go (n + 1)
 
 {- Enables an existing special remote. -}
 enableSpecialRemote :: SpecialRemoteMaker
-enableSpecialRemote name remotetype config = do
+enableSpecialRemote name remotetype mcreds config = do
 	r <- Command.InitRemote.findExisting name
 	case r of
 		Nothing -> error $ "Cannot find a special remote named " ++ name
-		Just (u, c) -> setupSpecialRemote name remotetype config (Just u, c)
+		Just (u, c) -> setupSpecialRemote name remotetype config mcreds (Just u, c)
 
-setupSpecialRemote :: RemoteName -> RemoteType -> R.RemoteConfig -> (Maybe UUID, R.RemoteConfig) -> Annex RemoteName
-setupSpecialRemote name remotetype config (mu, c) = do
+setupSpecialRemote :: RemoteName -> RemoteType -> R.RemoteConfig -> Maybe CredPair -> (Maybe UUID, R.RemoteConfig) -> Annex RemoteName
+setupSpecialRemote name remotetype config mcreds (mu, c) = do
 	{- Currently, only 'weak' ciphers can be generated from the
 	 - assistant, because otherwise GnuPG may block once the entropy
 	 - pool is drained, and as of now there's no way to tell the user
 	 - to perform IO actions to refill the pool. -}
-	(c', u) <- R.setup remotetype mu $
+	(c', u) <- R.setup remotetype mu mcreds $
 		M.insert "highRandomQuality" "false" $ M.union config c
 	describeUUID u name
 	configSet u c'
diff --git a/Assistant/Restart.hs b/Assistant/Restart.hs
--- a/Assistant/Restart.hs
+++ b/Assistant/Restart.hs
@@ -16,6 +16,7 @@
 import Utility.ThreadScheduler
 import Utility.NotificationBroadcaster
 import Utility.Url
+import Utility.PID
 import qualified Git.Construct
 import qualified Git.Config
 import Config.Files
@@ -25,10 +26,9 @@
 import Control.Concurrent
 import System.Process (cwd)
 #ifndef mingw32_HOST_OS
-import System.Posix (getProcessID, signalProcess, sigTERM)
+import System.Posix (signalProcess, sigTERM)
 #else
-import System.Win32.Process.Current (getCurrentProcessId)
-import System.Win32.Console (generateConsoleCtrlEvent, cTRL_C_EVENT)
+import Utility.WinProcess
 #endif
 
 {- Before the assistant can be restarted, have to remove our 
@@ -53,9 +53,9 @@
 	void $ liftIO $ forkIO $ do
 		threadDelaySeconds (Seconds 120)
 #ifndef mingw32_HOST_OS
-		signalProcess sigTERM =<< getProcessID
+		signalProcess sigTERM =<< getPID
 #else
-		generateConsoleCtrlEvent cTRL_C_EVENT =<< getCurrentProcessId
+		terminatePID =<< getPID
 #endif
 
 runRestart :: Assistant URLString
@@ -86,10 +86,13 @@
 		threadDelay 100000 -- 1/10th of a second
 		a
 
-{- Returns once the assistant has daemonized, but possibly before it's
- - listening for web connections. -}
+{- Does not wait for assistant to be listening for web connections. 
+ -
+ - On windows, the assistant does not daemonize, which is why the forkIO is
+ - done.
+ -}
 startAssistant :: FilePath -> IO ()
-startAssistant repo = do
+startAssistant repo = void $ forkIO $ do
 	program <- readProgramFile
 	(_, _, _, pid) <- 
 		createProcess $
diff --git a/Assistant/Threads/Merger.hs b/Assistant/Threads/Merger.hs
--- a/Assistant/Threads/Merger.hs
+++ b/Assistant/Threads/Merger.hs
@@ -80,8 +80,8 @@
 	mergecurrent (Just current)
 		| equivBranches changedbranch current = do
 			debug
-				[ "merging", show changedbranch
-				, "into", show current
+				[ "merging", Git.fromRef changedbranch
+				, "into", Git.fromRef current
 				]
 			void $ liftAnnex  $ Command.Sync.mergeFrom changedbranch
 	mergecurrent _ = noop
@@ -105,12 +105,12 @@
 equivBranches :: Git.Ref -> Git.Ref -> Bool
 equivBranches x y = base x == base y
   where
-	base = takeFileName . show
+	base = takeFileName . Git.fromRef
 
 isAnnexBranch :: FilePath -> Bool
 isAnnexBranch f = n `isSuffixOf` f
   where
-	n = '/' : show Annex.Branch.name
+	n = '/' : Git.fromRef Annex.Branch.name
 
 fileToBranch :: FilePath -> Git.Ref
 fileToBranch f = Git.Ref $ "refs" </> base
diff --git a/Assistant/Threads/SanityChecker.hs b/Assistant/Threads/SanityChecker.hs
--- a/Assistant/Threads/SanityChecker.hs
+++ b/Assistant/Threads/SanityChecker.hs
@@ -194,8 +194,13 @@
 
 hourlyCheck :: Assistant ()
 hourlyCheck = do
+#ifndef mingw32_HOST_OS
 	checkLogSize 0
+#else
+	noop
+#endif
 
+#ifndef mingw32_HOST_OS
 {- Rotate logs until log file size is < 1 mb. -}
 checkLogSize :: Int -> Assistant ()
 checkLogSize n = do
@@ -209,6 +214,7 @@
 			checkLogSize $ n + 1
   where
 	filesize f = fromIntegral . fileSize <$> liftIO (getFileStatus f)
+#endif
 
 oneMegabyte :: Int
 oneMegabyte = 1000000
diff --git a/Assistant/TransferSlots.hs b/Assistant/TransferSlots.hs
--- a/Assistant/TransferSlots.hs
+++ b/Assistant/TransferSlots.hs
@@ -39,7 +39,7 @@
 import System.Posix.Process (getProcessGroupIDOf)
 import System.Posix.Signals (signalProcessGroup, sigTERM, sigKILL)
 #else
-import System.Win32.Console (generateConsoleCtrlEvent, cTRL_C_EVENT, cTRL_BREAK_EVENT)
+import Utility.WinProcess
 #endif
 
 type TransferGenerator = Assistant (Maybe (Transfer, TransferInfo, Transferrer -> Assistant ()))
@@ -256,23 +256,19 @@
 	signalthread tid
 		| pause = throwTo tid PauseTransfer
 		| otherwise = killThread tid
-	{- In order to stop helper processes like rsync,
-	 - kill the whole process group of the process
-	 - running the transfer. -}
 	killproc pid = void $ tryIO $ do
 #ifndef mingw32_HOST_OS
+		{- In order to stop helper processes like rsync,
+		 - kill the whole process group of the process
+		 - running the transfer. -}
 		g <- getProcessGroupIDOf pid
 		let signal sig = void $ tryIO $ signalProcessGroup sig g
 		signal sigTERM
-		graceperiod
+		threadDelay 50000 -- 0.05 second grace period
 		signal sigKILL
 #else
-		let signal sig = void $ tryIO $ generateConsoleCtrlEvent sig pid
-		signal cTRL_C_EVENT
-		graceperiod
-		signal cTRL_BREAK_EVENT
+		terminatePID pid
 #endif
-	graceperiod = threadDelay 50000 -- 0.05 second
 
 {- Start or resume a transfer. -}
 startTransfer :: Transfer -> Assistant ()
diff --git a/Assistant/Types/NetMessager.hs b/Assistant/Types/NetMessager.hs
--- a/Assistant/Types/NetMessager.hs
+++ b/Assistant/Types/NetMessager.hs
@@ -32,7 +32,7 @@
 	| PairingNotification PairStage ClientID UUID
 	-- used for git push over the network messager
 	| Pushing ClientID PushStage
-	deriving (Show, Eq, Ord)
+	deriving (Eq, Ord, Show)
 
 {- Something used to identify the client, or clients to send the message to. -}
 type ClientID = Text
@@ -50,7 +50,7 @@
 	| SendPackOutput SequenceNum ByteString
 	-- sent when git receive-pack exits, with its exit code
 	| ReceivePackDone ExitCode
-	deriving (Show, Eq, Ord)
+	deriving (Eq, Ord, Show)
 
 {- A sequence number. Incremented by one per packet in a sequence,
  - starting with 1 for the first packet. 0 means sequence numbers are
diff --git a/Assistant/WebApp/Configurators/AWS.hs b/Assistant/WebApp/Configurators/AWS.hs
--- a/Assistant/WebApp/Configurators/AWS.hs
+++ b/Assistant/WebApp/Configurators/AWS.hs
@@ -202,11 +202,11 @@
 #endif
 
 makeAWSRemote :: SpecialRemoteMaker -> RemoteType -> StandardGroup -> AWSCreds -> RemoteName -> RemoteConfig -> Handler ()
-makeAWSRemote maker remotetype defaultgroup (AWSCreds ak sk) name config = do
-	liftIO $ AWS.setCredsEnv (T.unpack ak, T.unpack sk)
+makeAWSRemote maker remotetype defaultgroup (AWSCreds ak sk) name config = 
 	setupCloudRemote defaultgroup Nothing $
-		maker hostname remotetype config
+		maker hostname remotetype (Just creds) config
   where
+  	creds = (T.unpack ak, T.unpack sk)
 	{- AWS services use the remote name as the basis for a host
 	 - name, so filter it to contain valid characters. -}
 	hostname = case filter isAlphaNum name of
diff --git a/Assistant/WebApp/Configurators/Local.hs b/Assistant/WebApp/Configurators/Local.hs
--- a/Assistant/WebApp/Configurators/Local.hs
+++ b/Assistant/WebApp/Configurators/Local.hs
@@ -300,7 +300,6 @@
 getFinishAddDriveR :: RemovableDrive -> RepoKey -> Handler Html
 getFinishAddDriveR drive = go
   where
-  	{- Set up new gcrypt special remote. -}
 	go (RepoKey keyid) = whenGcryptInstalled $ makewith $ const $ do
 		r <- liftAnnex $ addRemote $
 			makeGCryptRemote remotename dir keyid
@@ -314,7 +313,7 @@
 		remotename' <- liftAnnex $ getGCryptRemoteName u dir
 		makewith $ const $ do
 			r <- liftAnnex $ addRemote $
-				enableSpecialRemote remotename' GCrypt.remote $ M.fromList
+				enableSpecialRemote remotename' GCrypt.remote Nothing $ M.fromList
 					[("gitrepo", dir)]
 			return (u, r)
 	{- Making a new unencrypted repo, or combining with an existing one. -}
diff --git a/Assistant/WebApp/Configurators/Ssh.hs b/Assistant/WebApp/Configurators/Ssh.hs
--- a/Assistant/WebApp/Configurators/Ssh.hs
+++ b/Assistant/WebApp/Configurators/Ssh.hs
@@ -25,6 +25,11 @@
 import Annex.UUID
 import Logs.UUID
 
+#ifdef mingw32_HOST_OS
+import Utility.Tmp
+import Utility.Rsync
+#endif
+
 import qualified Data.Text as T
 import qualified Data.Map as M
 import Network.Socket
@@ -354,7 +359,7 @@
 enableGCrypt :: SshData -> RemoteName -> Handler Html
 enableGCrypt sshdata reponame = 
 	setupCloudRemote TransferGroup Nothing $ 
-		enableSpecialRemote reponame GCrypt.remote $ M.fromList
+		enableSpecialRemote reponame GCrypt.remote Nothing $ M.fromList
 			[("gitrepo", genSshUrl sshdata)]
 
 {- Combining with a gcrypt repository that may not be
@@ -468,8 +473,18 @@
 	notencrypted = error "Unexpectedly found a non-encrypted git repository, instead of the expected encrypted git repository."
 	notinstalled = error "internal"
 
-{- Prepares rsync.net ssh key, and if successful, runs an action with
- - its SshData. -}
+{- Prepares rsync.net ssh key and creates the directory that will be 
+ - used on rsync.net. If successful, runs an action with its SshData.
+ -
+ - To append the ssh key to rsync.net's authorized_keys, their
+ - documentation recommends a dd methodd, where the line is fed
+ - in to ssh over stdin.
+ -
+ - On Windows, ssh password prompting happens on stdin, so cannot
+ - feed the key in that way. Instead, first rsync down any current
+ - authorized_keys file, then modifiy it, and then rsync it back up.
+ - This means 2 password prompts rather than one for Windows.
+ -}
 prepRsyncNet :: SshInput -> String -> (SshData -> Handler Html) -> Handler Html
 prepRsyncNet sshinput reponame a = do
 	knownhost <- liftIO $ maybe (return False) knownHost (inputHostname sshinput)
@@ -480,25 +495,37 @@
 			, needsPubKey = True
 			, sshCapabilities = [RsyncCapable]
 			}
+	let sshhost = genSshHost (sshHostName sshdata) (sshUserName sshdata)
+	let torsyncnet cmd = filter (not . null)
+		[ if knownhost then "" else sshOpt "StrictHostKeyChecking" "no"
+		, sshhost
+		, cmd
+		]
+#ifndef mingw32_HOST_OS
 	{- I'd prefer to separate commands with && , but
-	 - rsync.net's shell does not support that.
-	 -
-	 - The dd method of appending to the authorized_keys file is the
-	 - one recommended by rsync.net documentation. I touch the file first
-	 - to not need to use a different method to create it.
-	 -}
+	 - rsync.net's shell does not support that. -}
 	let remotecommand = intercalate ";"
 		[ "mkdir -p .ssh"
 		, "touch .ssh/authorized_keys"
 		, "dd of=.ssh/authorized_keys oflag=append conv=notrunc"
 		, "mkdir -p " ++ T.unpack (sshDirectory sshdata)
 		]
-	let sshopts = filter (not . null)
-		[ if knownhost then "" else sshOpt "StrictHostKeyChecking" "no"
-		, genSshHost (sshHostName sshdata) (sshUserName sshdata)
-		, remotecommand
-		]
-	sshSetup sshopts (Just $ sshPubKey keypair) $ a sshdata
+	sshSetup (torsyncnet remotecommand) (Just $ sshPubKey keypair) (a sshdata)
+#else
+	liftIO $ withTmpDir "rsyncnet" $ \tmpdir -> do
+		createDirectory $ tmpdir </> ".ssh"
+		(oldkeys, _) <- sshTranscript (torsyncnet "cat .ssh/authorized_keys") Nothing
+		writeFile (tmpdir </> ".ssh" </> "authorized_keys")
+			(sshPubKey keypair ++ "\n" ++ oldkeys)
+		liftIO $ putStrLn "May need to prompt for your rsync.net password one more time..."
+		void $ rsync
+			[ Param "-r"
+			, File $ tmpdir </> ".ssh/"
+			, Param $ sshhost ++ ":.ssh/"
+			]
+	let remotecommand = "mkdir -p " ++ T.unpack (sshDirectory sshdata)
+	sshSetup (torsyncnet remotecommand) Nothing (a sshdata)
+#endif
 
 isRsyncNet :: Maybe Text -> Bool
 isRsyncNet Nothing = False
diff --git a/Assistant/WebApp/Configurators/WebDAV.hs b/Assistant/WebApp/Configurators/WebDAV.hs
--- a/Assistant/WebApp/Configurators/WebDAV.hs
+++ b/Assistant/WebApp/Configurators/WebDAV.hs
@@ -123,10 +123,9 @@
 
 #ifdef WITH_WEBDAV
 makeWebDavRemote :: SpecialRemoteMaker -> RemoteName -> CredPair -> RemoteConfig -> Handler ()
-makeWebDavRemote maker name creds config = do
-	liftIO $ WebDAV.setCredsEnv creds
+makeWebDavRemote maker name creds config = 
 	setupCloudRemote TransferGroup Nothing $
-		maker name WebDAV.remote config
+		maker name WebDAV.remote (Just creds) config
 
 {- Only returns creds previously used for the same hostname. -}
 previouslyUsedWebDAVCreds :: String -> Annex (Maybe CredPair)
diff --git a/Assistant/WebApp/Control.hs b/Assistant/WebApp/Control.hs
--- a/Assistant/WebApp/Control.hs
+++ b/Assistant/WebApp/Control.hs
@@ -16,15 +16,15 @@
 import Assistant.Restart
 import Utility.LogFile
 import Utility.NotificationBroadcaster
+import Utility.PID
 
 import Control.Concurrent
 import qualified Data.Map as M
 import qualified Data.Text as T
 #ifndef mingw32_HOST_OS
-import System.Posix (getProcessID, signalProcess, sigTERM)
+import System.Posix (signalProcess, sigTERM)
 #else
-import System.Win32.Process.Current (getCurrentProcessId)
-import System.Win32.Console (generateConsoleCtrlEvent, cTRL_C_EVENT)
+import Utility.WinProcess
 #endif
 
 getShutdownR :: Handler Html
@@ -54,9 +54,9 @@
 	void $ liftIO $ forkIO $ do
 		threadDelay 2000000
 #ifndef mingw32_HOST_OS
-		signalProcess sigTERM =<< getProcessID
+		signalProcess sigTERM =<< getPID
 #else
-		generateConsoleCtrlEvent cTRL_C_EVENT =<< getCurrentProcessId
+		terminatePID =<< getPID
 #endif
 	redirect NotRunningR
 
diff --git a/Assistant/WebApp/DashBoard.hs b/Assistant/WebApp/DashBoard.hs
--- a/Assistant/WebApp/DashBoard.hs
+++ b/Assistant/WebApp/DashBoard.hs
@@ -130,8 +130,23 @@
 #endif
 	ifM (liftIO $ inPath cmd)
 		( do
-			void $ liftIO $ forkIO $ void $
+			let run = void $ liftIO $ forkIO $ void $
 				boolSystem cmd params
+			run
+#ifdef mingw32_HOST_OS
+			{- On windows, if the file browser is not
+			 - already open, it comes up below the
+			 - web browser when started. 
+			 -
+			 - Running it a second time brings it
+			 - to the foreground.
+			 -
+			 - Seems to need a delay long enough for the file
+			 - browser to be open in order to work. Here 1
+			 - second. -}
+			liftIO $ threadDelay 1000000
+			run
+#endif
 			return True
 		, do
 			void $ redirect $ "file://" ++ path
diff --git a/Assistant/WebApp/MakeRemote.hs b/Assistant/WebApp/MakeRemote.hs
--- a/Assistant/WebApp/MakeRemote.hs
+++ b/Assistant/WebApp/MakeRemote.hs
@@ -28,8 +28,8 @@
  - and finishes setting it up, then starts syncing with it,
  - and finishes by displaying the page to edit it. -}
 setupCloudRemote :: StandardGroup -> Maybe Cost -> Annex RemoteName -> Handler a
-setupCloudRemote defaultgroup mcost maker = do
-	r <- liftAnnex $ addRemote maker
+setupCloudRemote defaultgroup mcost name = do
+	r <- liftAnnex $ addRemote name
 	liftAnnex $ do
 		setStandardGroup (Remote.uuid r) defaultgroup
 		maybe noop (Config.setRemoteCost (Remote.repo r)) mcost
diff --git a/Assistant/XMPP.hs b/Assistant/XMPP.hs
--- a/Assistant/XMPP.hs
+++ b/Assistant/XMPP.hs
@@ -13,6 +13,7 @@
 import Assistant.Types.NetMessager
 import Assistant.Pairing
 import Git.Sha (extractSha)
+import Git
 
 import Network.Protocol.XMPP hiding (Node)
 import Data.Text (Text)
@@ -152,7 +153,7 @@
   where
 	encode (CanPush u shas) =
 		gitAnnexTag canPushAttr $ T.pack $ unwords $
-			fromUUID u : map show shas
+			fromUUID u : map fromRef shas
 	encode (PushRequest u) =
 		gitAnnexTag pushRequestAttr $ T.pack $ fromUUID u
 	encode (StartingPush u) =
diff --git a/Backend/Hash.hs b/Backend/Hash.hs
--- a/Backend/Hash.hs
+++ b/Backend/Hash.hs
@@ -101,6 +101,7 @@
 	case (mstat, fast) of
 		(Just stat, False) -> do
 			let filesize = fromIntegral $ fileSize stat
+			showSideAction "checksum"
 			check <$> hashFile hash file filesize
 		_ -> return True
   where
diff --git a/Build/DistributionUpdate.hs b/Build/DistributionUpdate.hs
--- a/Build/DistributionUpdate.hs
+++ b/Build/DistributionUpdate.hs
@@ -25,7 +25,7 @@
 	version <- liftIO getChangelogVersion
 	now <- liftIO getCurrentTime
 	liftIO $ putStrLn $ "building info files for version " ++ version ++ " in " ++ basedir
-	fs <- liftIO $ dirContentsRecursiveSkipping (== "info") True (basedir </> "git-annex")
+	fs <- liftIO $ dirContentsRecursiveSkipping (const False) True (basedir </> "git-annex")
 	forM_ fs $ \f -> do
 		v <- lookupFile f
 		case v of
@@ -54,6 +54,19 @@
 		[ Param "annex"
 		, Params "sync"
 		]
+	
+	{- Check for out of date info files. -}
+	infos <- liftIO $ filter (".info" `isSuffixOf`)
+		<$> dirContentsRecursive (basedir </> "git-annex")
+	ds <- liftIO $ forM infos (readish <$$> readFile)
+	let dis = zip infos ds
+	let ood = filter (outofdate version) dis
+	unless (null ood) $
+		error $ "Some info files are out of date: " ++ show (map fst ood)
+  where
+	outofdate version (_, md) = case md of
+		Nothing -> True
+		Just d -> distributionVersion d /= version
 
 getRepoDir :: IO FilePath
 getRepoDir = do
diff --git a/Build/EvilLinker.hs b/Build/EvilLinker.hs
--- a/Build/EvilLinker.hs
+++ b/Build/EvilLinker.hs
@@ -17,6 +17,7 @@
 import Control.Monad
 import System.Directory
 import Data.Maybe
+import Data.List
 
 import Utility.Monad
 import Utility.Process
@@ -94,13 +95,19 @@
 	path <- manyTill anyChar (try $ string ldcmd)
 	void $ char ' '
 	params <- restOfLine
-	return $ CmdParams (path ++ ldcmd) (escapeDosPaths params) Nothing
+	return $ CmdParams (path ++ ldcmd) (skipHack $ escapeDosPaths params) Nothing
   where
 	ldcmd = "ld.exe"
 	versionline = do
 		void $ string "collect2 version"
 		restOfLine
-	
+
+{- For unknown reasons, asking the linker to link this in fails,
+ - with error about multiple definitions of a symbol from the library.
+ - This is a horrible hack. -}
+skipHack :: String -> String
+skipHack = replace "dist/build/git-annex/git-annex-tmp/Utility/winprocess.o" ""
+
 {- Input contains something like 
  - c:/program files/haskell platform/foo -LC:/Program Files/Haskell Platform/ -L...
  - and the *right* spaces must be escaped with \
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,34 @@
+git-annex (5.20140221) unstable; urgency=medium
+
+  * metadata: New command that can attach metadata to files.
+  * --metadata can be used to limit commands to acting on files
+    that have particular metadata.
+  * Preferred content expressions can use metadata=field=value
+    to limit them to acting on files that have particular metadata.
+  * view: New command that creates and checks out a branch that provides
+    a structured view of selected metadata.
+  * vfilter, vadd, vpop, vcycle: New commands for operating within views.
+  * pre-commit: Update metadata when committing changes to locations
+    of annexed files within a view.
+  * Add progress display for transfers to/from external special remotes.
+  * unused: Fix to actually detect unused keys when in direct mode.
+  * fsck: When run with --all or --unused, while .gitattributes
+    annex.numcopies cannot be honored since it's operating on keys
+    instead of files, make it honor the global numcopies setting,
+    and the annex.numcopies git config setting.
+  * trust, untrust, semitrust, dead: Warn when the trust level is
+    overridden in .git/config.
+  * glacier: Do not try to run glacier value create when an existing glacier
+    remote is enabled.
+  * fsck: Refuse to do anything if more than one of --incremental, --more,
+    and --incremental-schedule are given, since it's not clear which option
+    should win.
+  * Windows webapp: Can set up box.com, Amazon S3, and rsync.net remotes
+  * Windows webapp: Can create repos on removable drives.
+  * Windows: Ensure HOME is set, as needed by bundled cygwin utilities.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 21 Feb 2014 11:23:59 -0400
+
 git-annex (5.20140210) unstable; urgency=medium
 
   * --in can now refer to files that were located in a repository at
diff --git a/CmdLine/GitAnnex.hs b/CmdLine/GitAnnex.hs
--- a/CmdLine/GitAnnex.hs
+++ b/CmdLine/GitAnnex.hs
@@ -26,6 +26,12 @@
 import qualified Command.TransferKey
 import qualified Command.TransferKeys
 import qualified Command.ReKey
+import qualified Command.MetaData
+import qualified Command.View
+import qualified Command.VAdd
+import qualified Command.VFilter
+import qualified Command.VPop
+import qualified Command.VCycle
 import qualified Command.Reinject
 import qualified Command.Fix
 import qualified Command.Init
@@ -134,6 +140,12 @@
 	, Command.TransferKey.def
 	, Command.TransferKeys.def
 	, Command.ReKey.def
+	, Command.MetaData.def
+	, Command.View.def
+	, Command.VAdd.def
+	, Command.VFilter.def
+	, Command.VPop.def
+	, Command.VCycle.def
 	, Command.Fix.def
 	, Command.Fsck.def
 	, Command.Repair.def
diff --git a/CmdLine/GitAnnex/Options.hs b/CmdLine/GitAnnex/Options.hs
--- a/CmdLine/GitAnnex/Options.hs
+++ b/CmdLine/GitAnnex/Options.hs
@@ -54,6 +54,8 @@
 		"match files larger than a size"
 	, Option [] ["smallerthan"] (ReqArg Limit.addSmallerThan paramSize)
 		"match files smaller than a size"
+	, Option [] ["metadata"] (ReqArg Limit.addMetaData "FIELD=VALUE")
+		"match files with attached metadata"
 	, Option [] ["want-get"] (NoArg Limit.Wanted.addWantGet)
 		"match files the repository wants to get"
 	, Option [] ["want-drop"] (NoArg Limit.Wanted.addWantDrop)
diff --git a/Command.hs b/Command.hs
--- a/Command.hs
+++ b/Command.hs
@@ -5,8 +5,6 @@
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
-{-# LANGUAGE BangPatterns #-}
-
 module Command (
 	command,
 	noRepo,
diff --git a/Command/Dead.hs b/Command/Dead.hs
--- a/Command/Dead.hs
+++ b/Command/Dead.hs
@@ -7,34 +7,13 @@
 
 module Command.Dead where
 
-import Common.Annex
 import Command
-import qualified Remote
-import Logs.Trust
-import Logs.Group
-
-import qualified Data.Set as S
+import Types.TrustLevel
+import Command.Trust (trustCommand)
 
 def :: [Command]
 def = [command "dead" (paramRepeating paramRemote) seek
 	SectionSetup "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
-	markDead uuid
-	next $ return True
-
-markDead :: UUID -> Annex ()
-markDead uuid = do
-	trustSet uuid DeadTrusted
-	groupSet uuid S.empty
+seek = trustCommand "dead" DeadTrusted
diff --git a/Command/EnableRemote.hs b/Command/EnableRemote.hs
--- a/Command/EnableRemote.hs
+++ b/Command/EnableRemote.hs
@@ -47,7 +47,7 @@
 
 perform :: RemoteType -> UUID -> R.RemoteConfig -> CommandPerform
 perform t u c = do
-	(c', u') <- R.setup t (Just u) c
+	(c', u') <- R.setup t (Just u) Nothing c
 	next $ cleanup u' c'
 
 cleanup :: UUID -> R.RemoteConfig -> CommandCleanup
diff --git a/Command/Fsck.hs b/Command/Fsck.hs
--- a/Command/Fsck.hs
+++ b/Command/Fsck.hs
@@ -31,12 +31,8 @@
 import Types.Key
 import Utility.HumanTime
 import Git.FilePath
+import Utility.PID
 
-#ifndef mingw32_HOST_OS
-import System.Posix.Process (getProcessID)
-#else
-import System.Win32.Process.Current (getCurrentProcessId)
-#endif
 import Data.Time.Clock.POSIX
 import Data.Time
 import System.Posix.Types (EpochTime)
@@ -72,7 +68,7 @@
 	from <- getOptionField fsckFromOption Remote.byNameWithUUID
 	i <- getIncremental
 	withKeyOptions
-		(startKey i)
+		(\k -> startKey i k =<< getNumCopies)
 		(withFilesInGit $ whenAnnexed $ start from i)
 		ps
 
@@ -84,11 +80,12 @@
 	morei <- Annex.getFlag (optionName moreIncrementalOption)
 	case (i, starti, morei) of
 		(False, False, False) -> return NonIncremental
-		(False, True, _) -> startIncremental
+		(False, True, False) -> startIncremental
 		(False ,False, True) -> ContIncremental <$> getStartTime
-		(True, _, _) ->
+		(True, False, False) ->
 			maybe startIncremental (return . ContIncremental . Just)
 				=<< getStartTime
+		_ -> error "Specify only one of --incremental, --more, or --incremental-schedule"
   where
 	startIncremental = do
 		recordStartTime
@@ -149,14 +146,10 @@
 		, checkKeyNumCopies key file numcopies
 		]
 	withtmp a = do
-#ifndef mingw32_HOST_OS
-		v <- liftIO getProcessID
-#else
-		v <- liftIO getCurrentProcessId
-#endif
+		pid <- liftIO getPID
 		t <- fromRepo gitAnnexTmpDir
 		createAnnexDirectory t
-		let tmp = t </> "fsck" ++ show v ++ "." ++ keyFile key
+		let tmp = t </> "fsck" ++ show pid ++ "." ++ keyFile key
 		let cleanup = liftIO $ catchIO (removeFile tmp) (const noop)
 		cleanup
 		cleanup `after` a tmp
@@ -170,18 +163,19 @@
 			)
 	dummymeter _ = noop
 
-startKey :: Incremental -> Key -> CommandStart
-startKey inc key = case Backend.maybeLookupBackendName (Types.Key.keyBackendName key) of
-	Nothing -> stop
-	Just backend -> runFsck inc (key2file key) key $ performAll key backend
+startKey :: Incremental -> Key -> NumCopies -> CommandStart
+startKey inc key numcopies =
+	case Backend.maybeLookupBackendName (Types.Key.keyBackendName key) of
+		Nothing -> stop
+		Just backend -> runFsck inc (key2file key) key $
+			performKey key backend numcopies
 
-{- Note that numcopies cannot be checked in --all mode, since we do not
- - have associated filenames to look up in the .gitattributes file. -}
-performAll :: Key -> Backend -> Annex Bool
-performAll key backend = check
+performKey :: Key -> Backend -> NumCopies -> Annex Bool
+performKey key backend numcopies = check
 	[ verifyLocationLog key (key2file key)
 	, checkKeySize key
 	, checkBackend backend key Nothing
+	, checkKeyNumCopies key (key2file key) numcopies
 	]
 
 check :: [Annex Bool] -> Annex Bool
@@ -365,7 +359,7 @@
 				, return True
 				)
 
-checkKeyNumCopies :: Key -> FilePath -> NumCopies -> Annex Bool
+checkKeyNumCopies :: Key -> String -> NumCopies -> Annex Bool
 checkKeyNumCopies key file numcopies = do
 	(untrustedlocations, safelocations) <- trustPartition UnTrusted =<< Remote.keyLocations key
 	let present = NumCopies (length safelocations)
@@ -415,7 +409,7 @@
 		++ Remote.name remote
 
 data Incremental = StartIncremental | ContIncremental (Maybe EpochTime) | NonIncremental
-	deriving (Eq)
+	deriving (Eq, Show)
 
 runFsck :: Incremental -> FilePath -> Key -> Annex Bool -> CommandStart
 runFsck inc file key a = ifM (needFsck inc key)
@@ -471,7 +465,9 @@
  -
  - To guard against time stamp damange (for example, if an annex directory
  - is copied without -a), the fsckstate file contains a time that should
- - be identical to its modification time. -}
+ - be identical to its modification time.
+ - (This is not possible to do on Windows.)
+ -}
 recordStartTime :: Annex ()
 recordStartTime = do
 	f <- fromRepo gitAnnexFsckState
@@ -479,8 +475,12 @@
 	liftIO $ do
 		nukeFile f
 		withFile f WriteMode $ \h -> do
+#ifndef mingw32_HOST_OS
 			t <- modificationTime <$> getFileStatus f
 			hPutStr h $ showTime $ realToFrac t
+#else
+			noop
+#endif
   where
 	showTime :: POSIXTime -> String
 	showTime = show
@@ -494,10 +494,14 @@
 	f <- fromRepo gitAnnexFsckState
 	liftIO $ catchDefaultIO Nothing $ do
 		timestamp <- modificationTime <$> getFileStatus f
+#ifndef mingw32_HOST_OS
 		t <- readishTime <$> readFile f
 		return $ if Just (realToFrac timestamp) == t
 			then Just timestamp
 			else Nothing
+#else
+		return $ Just timestamp
+#endif
   where
 	readishTime :: String -> Maybe POSIXTime
 	readishTime s = utcTimeToPOSIXSeconds <$>
diff --git a/Command/InitRemote.hs b/Command/InitRemote.hs
--- a/Command/InitRemote.hs
+++ b/Command/InitRemote.hs
@@ -44,7 +44,7 @@
 
 perform :: RemoteType -> String -> R.RemoteConfig -> CommandPerform
 perform t name c = do
-	(c', u) <- R.setup t Nothing c
+	(c', u) <- R.setup t Nothing Nothing c
 	next $ cleanup u name c'
 
 cleanup :: UUID -> String -> R.RemoteConfig -> CommandCleanup
diff --git a/Command/Log.hs b/Command/Log.hs
--- a/Command/Log.hs
+++ b/Command/Log.hs
@@ -140,7 +140,7 @@
 		[ Params "log -z --pretty=format:%ct --raw --abbrev=40"
 		, Param "--remove-empty"
 		] ++ os ++
-		[ Param $ show Annex.Branch.fullname
+		[ Param $ Git.fromRef Annex.Branch.fullname
 		, Param "--"
 		, Param logfile
 		]
diff --git a/Command/MetaData.hs b/Command/MetaData.hs
new file mode 100644
--- /dev/null
+++ b/Command/MetaData.hs
@@ -0,0 +1,68 @@
+{- git-annex command
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Command.MetaData where
+
+import Common.Annex
+import qualified Annex
+import Command
+import Logs.MetaData
+import Types.MetaData
+
+import qualified Data.Set as S
+import Data.Time.Clock.POSIX
+
+def :: [Command]
+def = [withOptions [setOption, tagOption, untagOption] $
+	command "metadata" paramPaths seek
+	SectionMetaData "sets metadata of a file"]
+
+storeModMeta :: ModMeta -> Annex ()
+storeModMeta modmeta = Annex.changeState $
+	\s -> s { Annex.modmeta = modmeta:Annex.modmeta s }
+
+setOption :: Option
+setOption = Option ['s'] ["set"] (ReqArg mkmod "FIELD[+-]=VALUE") "set metadata"
+  where
+	mkmod = either error storeModMeta . parseModMeta
+
+tagOption :: Option
+tagOption = Option ['t'] ["tag"] (ReqArg mkmod "TAG") "set a tag"
+  where
+	mkmod = storeModMeta . AddMeta tagMetaField . toMetaValue
+
+untagOption :: Option
+untagOption = Option ['u'] ["untag"] (ReqArg mkmod "TAG") "remove a tag"
+  where
+	mkmod = storeModMeta . AddMeta tagMetaField . mkMetaValue (CurrentlySet False)
+
+seek :: CommandSeek
+seek ps = do
+	modmeta <- Annex.getState Annex.modmeta
+	now <- liftIO getPOSIXTime
+	withFilesInGit (whenAnnexed $ start now modmeta) ps
+
+start :: POSIXTime -> [ModMeta] -> FilePath -> (Key, Backend) -> CommandStart
+start now ms file (k, _) = do
+	showStart "metadata" file
+	next $ perform now ms k
+
+perform :: POSIXTime -> [ModMeta] -> Key -> CommandPerform
+perform _ [] k = next $ cleanup k
+perform now ms k = do
+	oldm <- getCurrentMetaData k
+	let m = foldl' unionMetaData newMetaData $ map (modMeta oldm) ms
+	addMetaData' k m now
+	next $ cleanup k
+	
+cleanup :: Key -> CommandCleanup
+cleanup k = do
+	m <- getCurrentMetaData k
+	showLongNote $ unlines $ concatMap showmeta $ fromMetaData $ currentMetaData m
+	return True
+  where
+	showmeta (f, vs) = map (\v -> fromMetaField f ++ "=" ++ fromMetaValue v) $ S.toList vs
diff --git a/Command/PreCommit.hs b/Command/PreCommit.hs
--- a/Command/PreCommit.hs
+++ b/Command/PreCommit.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010, 2013 Joey Hess <joey@kitenet.net>
+ - Copyright 2010-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -13,7 +13,14 @@
 import qualified Command.Add
 import qualified Command.Fix
 import Annex.Direct
+import Annex.View
+import Logs.View
+import Logs.MetaData
+import Types.View
+import Types.MetaData
 
+import qualified Data.Set as S
+
 def :: [Command]
 def = [command "pre-commit" paramPaths seek SectionPlumbing
 	"run by git pre-commit hook"]
@@ -27,13 +34,45 @@
 		withFilesToBeCommitted (whenAnnexed Command.Fix.start) ps
 		-- inject unlocked files into the annex
 		withFilesUnlockedToBeCommitted startIndirect ps
+		-- committing changes to a view updates metadata
+		mv <- currentView
+		case mv of
+			Nothing -> noop
+			Just v -> withViewChanges
+				(addViewMetaData v)
+				(removeViewMetaData v)
 	)
 
 startIndirect :: FilePath -> CommandStart
-startIndirect file = next $ do
-	unlessM (callCommandAction $ Command.Add.start file) $
-		error $ "failed to add " ++ file ++ "; canceling commit"
+startIndirect f = next $ do
+	unlessM (callCommandAction $ Command.Add.start f) $
+		error $ "failed to add " ++ f ++ "; canceling commit"
 	next $ return True
 
 startDirect :: [String] -> CommandStart
 startDirect _ = next $ next $ preCommitDirect
+
+addViewMetaData :: View -> FileView -> Key -> CommandStart
+addViewMetaData v f k = do
+	showStart "metadata" f
+	next $ next $ changeMetaData k $ fromView v f
+
+removeViewMetaData :: View -> FileView -> Key -> CommandStart
+removeViewMetaData v f k = do
+	showStart "metadata" f
+	next $ next $ changeMetaData k $ unsetMetaData $ fromView v f
+
+changeMetaData :: Key -> MetaData -> CommandCleanup
+changeMetaData k metadata = do
+	showMetaDataChange metadata
+	addMetaData k metadata
+	return True
+
+showMetaDataChange :: MetaData -> Annex ()
+showMetaDataChange = showLongNote . unlines . concatMap showmeta . fromMetaData
+  where
+	showmeta (f, vs) = map (showmetavalue f) $ S.toList vs
+	showmetavalue f v = fromMetaField f ++ showset v ++ "=" ++ fromMetaValue v
+	showset v
+		| isSet v = "+"
+		| otherwise = "-"
diff --git a/Command/Repair.hs b/Command/Repair.hs
--- a/Command/Repair.hs
+++ b/Command/Repair.hs
@@ -81,4 +81,4 @@
 trackingOrSyncBranch b = Git.Repair.isTrackingBranch b || isAnnexSyncBranch b
 
 isAnnexSyncBranch :: Ref -> Bool
-isAnnexSyncBranch b = "refs/synced/" `isPrefixOf` show b
+isAnnexSyncBranch b = "refs/synced/" `isPrefixOf` fromRef b
diff --git a/Command/Semitrust.hs b/Command/Semitrust.hs
--- a/Command/Semitrust.hs
+++ b/Command/Semitrust.hs
@@ -7,26 +7,13 @@
 
 module Command.Semitrust where
 
-import Common.Annex
 import Command
-import qualified Remote
-import Logs.Trust
+import Types.TrustLevel
+import Command.Trust (trustCommand)
 
 def :: [Command]
 def = [command "semitrust" (paramRepeating paramRemote) seek
 	SectionSetup "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
+seek = trustCommand "semitrust" SemiTrusted
diff --git a/Command/Sync.hs b/Command/Sync.hs
--- a/Command/Sync.hs
+++ b/Command/Sync.hs
@@ -86,7 +86,7 @@
 		,  [ mergeAnnex ]
 		]
 	whenM (Annex.getFlag $ optionName contentOption) $
-		whenM (seekSyncContent dataremotes) $ do
+		whenM (seekSyncContent dataremotes) $
 			-- Transferring content can take a while,
 			-- and other changes can be pushed to the git-annex
 			-- branch on the remotes in the meantime, so pull
@@ -192,12 +192,12 @@
 
 updateBranch :: Git.Ref -> Git.Repo -> IO ()
 updateBranch syncbranch g = 
-	unlessM go $ error $ "failed to update " ++ show syncbranch
+	unlessM go $ error $ "failed to update " ++ Git.fromRef syncbranch
   where
 	go = Git.Command.runBool
 		[ Param "branch"
 		, Param "-f"
-		, Param $ show $ Git.Ref.base syncbranch
+		, Param $ Git.fromRef $ Git.Ref.base syncbranch
 		] g
 
 pullRemote :: Remote -> Maybe Git.Ref -> CommandStart
@@ -224,7 +224,7 @@
 	Just _ -> and <$> (mapM merge =<< tomerge (branchlist b))
   where
 	merge = mergeFrom . remoteBranch remote
-	tomerge branches = filterM (changed remote) branches
+	tomerge = filterM (changed remote)
 	branchlist Nothing = []
 	branchlist (Just branch) = [branch, syncBranch branch]
 
@@ -283,15 +283,15 @@
 		, refspec branch
 		]
 	directpush = Git.Command.runQuiet $ pushparams
-		[show $ Git.Ref.base $ fromDirectBranch branch]
+		[Git.fromRef $ Git.Ref.base $ fromDirectBranch branch]
 	pushparams branches =
 		[ Param "push"
 		, Param $ Remote.name remote
 		] ++ map Param branches
 	refspec b = concat 
-		[ show $ Git.Ref.base b
+		[ Git.fromRef $ Git.Ref.base b
 		,  ":"
-		, show $ Git.Ref.base $ syncBranch b
+		, Git.fromRef $ Git.Ref.base $ syncBranch b
 		]
 
 commitAnnex :: CommandStart
@@ -452,7 +452,7 @@
 			Just target -> do
 				unlessM (coreSymlinks <$> Annex.getGitConfig) $
 					addAnnexLink target f
-				maybe noop (flip toDirect f) 
+				maybe noop (`toDirect` f) 
 					(fileKey (takeFileName target))
 
 {- git-merge moves conflicting files away to files
@@ -535,7 +535,7 @@
  -}
 seekSyncContent :: [Remote] -> Annex Bool
 seekSyncContent rs = do
-	mvar <- liftIO $ newEmptyMVar
+	mvar <- liftIO newEmptyMVar
 	mapM_ (go mvar) =<< seekHelper LsFiles.inRepo []
 	liftIO $ not <$> isEmptyMVar mvar
   where
@@ -552,7 +552,7 @@
 	putrs <- catMaybes . snd . unzip <$> (sequence =<< handleput lack)
 
 	u <- getUUID
-	let locs' = concat [if got then [u] else [], putrs, locs]
+	let locs' = concat [[u | got], putrs, locs]
 
 	-- Using callCommandAction rather than commandAction for drops,
 	-- because a failure to drop does not mean the sync failed.
@@ -576,7 +576,7 @@
 		| Remote.readonly r || remoteAnnexReadOnly (Types.Remote.gitconfig r) = return False
 		| otherwise = wantSend True (Just k) (Just f) (Remote.uuid r)
 	handleput lack = ifM (inAnnex k)
-		( map put <$> (filterM wantput lack)
+		( map put <$> filterM wantput lack
 		, return []
 		)
 	put dest = do
diff --git a/Command/Trust.hs b/Command/Trust.hs
--- a/Command/Trust.hs
+++ b/Command/Trust.hs
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010 Joey Hess <joey@kitenet.net>
+ - Copyright 2010, 2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -10,23 +10,32 @@
 import Common.Annex
 import Command
 import qualified Remote
+import Types.TrustLevel
 import Logs.Trust
+import Logs.Group
 
+import qualified Data.Set as S
+
 def :: [Command]
 def = [command "trust" (paramRepeating paramRemote) seek
 	SectionSetup "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
+seek = trustCommand "trust" Trusted
 
-perform :: UUID -> CommandPerform
-perform uuid = do
-	trustSet uuid Trusted
-	next $ return True
+trustCommand :: String -> TrustLevel -> CommandSeek
+trustCommand cmd level = withWords start
+  where
+	start ws = do
+		let name = unwords ws
+		showStart cmd name
+		u <- Remote.nameToUUID name
+		next $ perform u
+	perform uuid = do
+		trustSet uuid level
+		when (level == DeadTrusted) $
+			groupSet uuid S.empty
+		l <- lookupTrust uuid
+		when (l /= level) $
+			warning $ "This remote's trust level is locally overridden to " ++ showTrustLevel l ++ " via git config."
+		next $ return True
diff --git a/Command/Uninit.hs b/Command/Uninit.hs
--- a/Command/Uninit.hs
+++ b/Command/Uninit.hs
@@ -24,7 +24,7 @@
 check = do
 	b <- current_branch
 	when (b == Annex.Branch.name) $ error $
-		"cannot uninit when the " ++ show b ++ " branch is checked out"
+		"cannot uninit when the " ++ Git.fromRef b ++ " branch is checked out"
 	top <- fromRepo Git.repoPath
 	cwd <- liftIO getCurrentDirectory
 	whenM ((/=) <$> liftIO (absPath top) <*> liftIO (absPath cwd)) $
@@ -77,7 +77,7 @@
 	-- avoid normal shutdown
 	saveState False
 	inRepo $ Git.Command.run
-		[Param "branch", Param "-D", Param $ show Annex.Branch.name]
+		[Param "branch", Param "-D", Param $ Git.fromRef Annex.Branch.name]
 	liftIO exitSuccess
 
 {- Keys that were moved out of the annex have a hard link still in the
diff --git a/Command/Untrust.hs b/Command/Untrust.hs
--- a/Command/Untrust.hs
+++ b/Command/Untrust.hs
@@ -7,26 +7,13 @@
 
 module Command.Untrust where
 
-import Common.Annex
 import Command
-import qualified Remote
-import Logs.Trust
+import Types.TrustLevel
+import Command.Trust (trustCommand)
 
 def :: [Command]
 def = [command "untrust" (paramRepeating paramRemote) seek
 	SectionSetup "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
+seek = trustCommand "untrust" UnTrusted
diff --git a/Command/Unused.hs b/Command/Unused.hs
--- a/Command/Unused.hs
+++ b/Command/Unused.hs
@@ -266,7 +266,7 @@
 		map (separate (== ' ')) .
 		lines
 	nubRefs = map (Git.Ref . snd) . nubBy (\(x, _) (y, _) -> x == y)
-	ourbranchend = '/' : show Annex.Branch.name
+	ourbranchend = '/' : Git.fromRef Annex.Branch.name
 	ourbranches (_, b) = not (ourbranchend `isSuffixOf` b)
 		&& not ("refs/synced/" `isPrefixOf` b)
 	addHead headRef refs = case headRef of
diff --git a/Command/VAdd.hs b/Command/VAdd.hs
new file mode 100644
--- /dev/null
+++ b/Command/VAdd.hs
@@ -0,0 +1,36 @@
+{- git-annex command
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Command.VAdd where
+
+import Common.Annex
+import Command
+import Annex.View
+import Command.View (paramView, parseViewParam, checkoutViewBranch)
+
+def :: [Command]
+def = [notBareRepo $ notDirect $
+	command "vadd" paramView seek SectionMetaData "add subdirs to current view"]
+
+seek :: CommandSeek
+seek = withWords start
+
+start :: [String] -> CommandStart
+start params = do
+	showStart "vadd" ""
+	withCurrentView $ \view -> do
+		let (view', change) = refineView view $
+			map parseViewParam $ reverse params
+		case change of
+			Unchanged -> do
+				showNote "unchanged"
+				next $ next $ return True
+			Narrowing -> next $ next $ do
+				if visibleViewSize view' == visibleViewSize view
+					then error "That would not add an additional level of directory structure to the view. To filter the view, use vfilter instead of vadd."
+					else checkoutViewBranch view' narrowView
+			Widening -> error "Widening view to match more files is not currently supported."
diff --git a/Command/VCycle.hs b/Command/VCycle.hs
new file mode 100644
--- /dev/null
+++ b/Command/VCycle.hs
@@ -0,0 +1,41 @@
+{- git-annex command
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Command.VCycle where
+
+import Common.Annex
+import Command
+import Annex.View
+import Types.View
+import Logs.View
+import Command.View (checkoutViewBranch)
+
+def :: [Command]
+def = [notBareRepo $ notDirect $
+	command "vcycle" paramNothing seek SectionUtility
+	"switch view to next layout"]
+
+seek :: CommandSeek
+seek = withNothing start
+
+start ::CommandStart
+start = go =<< currentView
+  where
+	go Nothing = error "Not in a view."
+	go (Just v) = do
+		showStart "vcycle" ""
+		let v' = v { viewComponents = vcycle [] (viewComponents v) }
+		if v == v'
+			then do
+				showNote "unchanged"
+				next $ next $ return True
+			else next $ next $ checkoutViewBranch v' narrowView
+
+	vcycle rest (c:cs)
+		| multiValue (viewFilter c) = rest ++ cs ++ [c]
+		| otherwise = vcycle (c:rest) cs
+	vcycle rest c = rest ++ c
diff --git a/Command/VFilter.hs b/Command/VFilter.hs
new file mode 100644
--- /dev/null
+++ b/Command/VFilter.hs
@@ -0,0 +1,30 @@
+{- git-annex command
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Command.VFilter where
+
+import Common.Annex
+import Command
+import Annex.View
+import Command.View (paramView, parseViewParam, checkoutViewBranch)
+
+def :: [Command]
+def = [notBareRepo $ notDirect $
+	command "vfilter" paramView seek SectionMetaData "filter current view"]
+
+seek :: CommandSeek
+seek = withWords start
+
+start :: [String] -> CommandStart
+start params = do
+	showStart "vfilter" ""
+	withCurrentView $ \view -> do
+		let view' = filterView view $
+			map parseViewParam $ reverse params
+		next $ next $ if visibleViewSize view' > visibleViewSize view
+			then error "That would add an additional level of directory structure to the view, rather than filtering it. If you want to do that, use vadd instead of vfilter."
+			else checkoutViewBranch view' narrowView
diff --git a/Command/VPop.hs b/Command/VPop.hs
new file mode 100644
--- /dev/null
+++ b/Command/VPop.hs
@@ -0,0 +1,50 @@
+{- git-annex command
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Command.VPop where
+
+import Common.Annex
+import Command
+import qualified Git
+import qualified Git.Command
+import qualified Git.Ref
+import Types.View
+import Logs.View
+import Command.View (checkoutViewBranch)
+
+def :: [Command]
+def = [notBareRepo $ notDirect $
+	command "vpop" (paramOptional paramNumber) seek SectionMetaData
+	"switch back to previous view"]
+
+seek :: CommandSeek
+seek = withWords start
+
+start :: [String] -> CommandStart
+start ps = go =<< currentView
+  where
+	go Nothing = error "Not in a view."
+	go (Just v) = do
+		showStart "vpop" (show num)
+		removeView v
+		(oldvs, vs) <- splitAt (num - 1) . filter (sameparentbranch v)
+			<$> recentViews
+		mapM_ removeView oldvs
+		case vs of
+			(oldv:_) -> next $ next $ do
+				showOutput
+				checkoutViewBranch oldv (return . branchView)
+			_ -> next $ next $ do
+				showOutput
+				inRepo $ Git.Command.runBool
+					[ Param "checkout"
+					, Param $ Git.fromRef $ Git.Ref.base $
+						viewParentBranch v
+					]
+	sameparentbranch a b = viewParentBranch a == viewParentBranch b
+
+	num = fromMaybe 1 $ readish =<< headMaybe ps 
diff --git a/Command/View.hs b/Command/View.hs
new file mode 100644
--- /dev/null
+++ b/Command/View.hs
@@ -0,0 +1,90 @@
+{- git-annex command
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Command.View where
+
+import Common.Annex
+import Command
+import qualified Git
+import qualified Git.Command
+import qualified Git.Ref
+import qualified Git.Branch
+import Types.MetaData
+import Types.View
+import Annex.View
+import Logs.View
+
+def :: [Command]
+def = [notBareRepo $ notDirect $
+	command "view" paramView seek SectionMetaData "enter a view branch"]
+
+seek :: CommandSeek
+seek = withWords start
+
+start :: [String] -> CommandStart
+start [] = error "Specify metadata to include in view"
+start params = do
+	showStart "view" ""
+	view <- mkView params
+	go view  =<< currentView
+  where
+	go view Nothing = next $ perform view
+	go view (Just v)
+		| v == view = stop
+		| otherwise = error "Already in a view. Use 'git annex vadd' to further refine this view."
+
+perform :: View -> CommandPerform
+perform view = do
+	showSideAction "searching"
+	next $ checkoutViewBranch view applyView
+
+paramView :: String
+paramView = paramPair (paramRepeating "FIELD=VALUE") (paramRepeating "TAG")
+
+parseViewParam :: String -> (MetaField, String)
+parseViewParam s = case separate (== '=') s of
+	(tag, []) -> (tagMetaField, tag)
+	(field, wanted) -> either error (\f -> (f, wanted)) (mkMetaField field)
+
+mkView :: [String] -> Annex View
+mkView params = do
+	v <- View <$> viewbranch <*> pure []
+	return $ fst $ refineView v $
+		map parseViewParam $ reverse params
+  where
+	viewbranch = fromMaybe (error "not on any branch!")
+		<$> inRepo Git.Branch.current
+
+checkoutViewBranch :: View -> (View -> Annex Git.Branch) -> CommandCleanup
+checkoutViewBranch view mkbranch = do
+	oldcwd <- liftIO getCurrentDirectory
+
+	{- Change to top of repository before creating view branch. -}
+	liftIO . setCurrentDirectory =<< fromRepo Git.repoPath
+	branch <- mkbranch view
+	
+	showOutput
+	ok <- inRepo $ Git.Command.runBool
+		[ Param "checkout"
+		, Param (Git.fromRef $ Git.Ref.base branch)
+		]
+	when ok $ do
+		setView view
+		{- A git repo can easily have empty directories in it,
+		 - and this pollutes the view, so remove them. -}
+		liftIO $ removeemptydirs "."
+		unlessM (liftIO $ doesDirectoryExist oldcwd) $ do
+			top <- fromRepo Git.repoPath
+			showLongNote (cwdmissing top)
+	return ok
+  where
+	removeemptydirs top = mapM_ (tryIO . removeDirectory)
+		=<< dirTreeRecursiveSkipping (".git" `isSuffixOf`) top
+	cwdmissing top = unlines
+		[ "This view does not include the subdirectory you are currently in."
+		, "Perhaps you should:  cd " ++ top
+		]
diff --git a/Creds.hs b/Creds.hs
--- a/Creds.hs
+++ b/Creds.hs
@@ -1,29 +1,34 @@
 {- Credentials storage
  -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2012-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
-{-# LANGUAGE CPP #-}
-
-module Creds where
+module Creds (
+	module Types.Creds,
+	CredPairStorage(..),
+	setRemoteCredPair,
+	getRemoteCredPairFor,
+	getRemoteCredPair,
+	getEnvCredPair,
+	writeCacheCreds,
+	readCacheCreds,
+) where
 
 import Common.Annex
+import Types.Creds
 import Annex.Perms
 import Utility.FileMode
 import Crypto
 import Types.Remote (RemoteConfig, RemoteConfigKey)
 import Remote.Helper.Encryptable (remoteCipher, embedCreds)
-import Utility.Env (setEnv, getEnv)
+import Utility.Env (getEnv)
 
 import qualified Data.ByteString.Lazy.Char8 as L
 import qualified Data.Map as M
 import Utility.Base64
 
-type Creds = String -- can be any data
-type CredPair = (String, String) -- login, password
-
 {- A CredPair can be stored in a file, or in the environment, or perhaps
  - in a remote's configuration. -}
 data CredPairStorage = CredPairStorage
@@ -33,14 +38,13 @@
 	}
 
 {- Stores creds in a remote's configuration, if the remote allows
- - that. Otherwise, caches them locally. -}
-setRemoteCredPair :: RemoteConfig -> CredPairStorage -> Annex RemoteConfig
-setRemoteCredPair c storage = 
-	maybe (return c) (setRemoteCredPair' c storage)
+ - that. Otherwise, caches them locally.
+ - The creds are found in storage if not provided. -}
+setRemoteCredPair :: RemoteConfig -> CredPairStorage -> Maybe CredPair -> Annex RemoteConfig
+setRemoteCredPair c storage Nothing = 
+	maybe (return c) (setRemoteCredPair c storage . Just)
 		=<< getRemoteCredPair c storage
-
-setRemoteCredPair' :: RemoteConfig -> CredPairStorage -> CredPair -> Annex RemoteConfig
-setRemoteCredPair' c storage creds
+setRemoteCredPair c storage (Just creds)
 	| embedCreds c = case credPairRemoteKey storage of
 		Nothing -> localcache
 		Just key -> storeconfig key =<< remoteCipher c
@@ -104,19 +108,6 @@
 	<*> getEnv penv
   where
 	(uenv, penv) = credPairEnvironment storage
-
-{- Stores a CredPair in the environment. -}
-setEnvCredPair :: CredPair -> CredPairStorage -> IO ()
-#ifndef mingw32_HOST_OS
-setEnvCredPair (l, p) storage = do
-	set uenv l
-	set penv p
-  where
-	(uenv, penv) = credPairEnvironment storage
-	set var val = void $ setEnv var val True
-#else
-setEnvCredPair _ _ = error "setEnvCredPair TODO"
-#endif
 
 writeCacheCredPair :: CredPair -> CredPairStorage -> Annex ()
 writeCacheCredPair credpair storage =
diff --git a/Git.hs b/Git.hs
--- a/Git.hs
+++ b/Git.hs
@@ -13,6 +13,7 @@
 module Git (
 	Repo(..),
 	Ref(..),
+	fromRef,
 	Branch,
 	Sha,
 	Tag,
diff --git a/Git/Branch.hs b/Git/Branch.hs
--- a/Git/Branch.hs
+++ b/Git/Branch.hs
@@ -28,7 +28,7 @@
 	case v of
 		Nothing -> return Nothing
 		Just branch -> 
-			ifM (null <$> pipeReadStrict [Param "show-ref", Param $ show branch] r)
+			ifM (null <$> pipeReadStrict [Param "show-ref", Param $ fromRef branch] r)
 				( return Nothing
 				, return v
 				)
@@ -36,7 +36,7 @@
 {- The current branch, which may not really exist yet. -}
 currentUnsafe :: Repo -> IO (Maybe Git.Ref)
 currentUnsafe r = parse . firstLine
-	<$> pipeReadStrict [Param "symbolic-ref", Param $ show Git.Ref.headRef] r
+	<$> pipeReadStrict [Param "symbolic-ref", Param $ fromRef Git.Ref.headRef] r
   where
 	parse l
 		| null l = Nothing
@@ -51,7 +51,7 @@
   where
 	diffs = pipeReadStrict
 		[ Param "log"
-		, Param (show origbranch ++ ".." ++ show newbranch)
+		, Param (fromRef origbranch ++ ".." ++ fromRef newbranch)
 		, Params "--oneline -n1"
 		] repo
 
@@ -74,7 +74,7 @@
   where
 	no_ff = return False
 	do_ff to = do
-		run [Param "update-ref", Param $ show branch, Param $ show to] repo
+		run [Param "update-ref", Param $ fromRef branch, Param $ fromRef to] repo
 		return True
 	findbest c [] = return $ Just c
 	findbest c (r:rs)
@@ -104,14 +104,14 @@
 	ifM (cancommit tree)
 		( do
 			sha <- getSha "commit-tree" $ pipeWriteRead
-				(map Param $ ["commit-tree", show tree] ++ ps)
+				(map Param $ ["commit-tree", fromRef tree] ++ ps)
 				(Just $ flip hPutStr message) repo
 			update branch sha repo
 			return $ Just sha
 		, return Nothing
 		)
   where
-	ps = concatMap (\r -> ["-p", show r]) parentrefs
+	ps = concatMap (\r -> ["-p", fromRef r]) parentrefs
 	cancommit tree
 		| allowempty = return True
 		| otherwise = case parentrefs of
@@ -130,8 +130,8 @@
 update :: Branch -> Sha -> Repo -> IO ()
 update branch sha = run 
 	[ Param "update-ref"
-	, Param $ show branch
-	, Param $ show sha
+	, Param $ fromRef branch
+	, Param $ fromRef sha
 	]
 
 {- Checks out a branch, creating it if necessary. -}
@@ -140,7 +140,7 @@
 	[ Param "checkout"
 	, Param "-q"
 	, Param "-B"
-	, Param $ show $ Git.Ref.base branch
+	, Param $ fromRef $ Git.Ref.base branch
 	]
 
 {- Removes a branch. -}
@@ -149,5 +149,5 @@
 	[ Param "branch"
 	, Param "-q"
 	, Param "-D"
-	, Param $ show $ Git.Ref.base branch
+	, Param $ fromRef $ Git.Ref.base branch
 	]
diff --git a/Git/CatFile.hs b/Git/CatFile.hs
--- a/Git/CatFile.hs
+++ b/Git/CatFile.hs
@@ -50,7 +50,7 @@
 {- Reads a file from a specified branch. -}
 catFile :: CatFileHandle -> Branch -> FilePath -> IO L.ByteString
 catFile h branch file = catObject h $ Ref $
-	show branch ++ ":" ++ toInternalGitPath file
+	fromRef branch ++ ":" ++ toInternalGitPath file
 
 {- Uses a running git cat-file read the content of an object.
  - Objects that do not exist will have "" returned. -}
@@ -60,7 +60,7 @@
 catObjectDetails :: CatFileHandle -> Ref -> IO (Maybe (L.ByteString, Sha, ObjectType))
 catObjectDetails (CatFileHandle hdl _) object = CoProcess.query hdl send receive
   where
-	query = show object
+	query = fromRef object
 	send to = hPutStrLn to query
 	receive from = do
 		header <- hGetLine from
@@ -72,8 +72,8 @@
 						_ -> dne
 				| otherwise -> dne
 			_
-				| header == show object ++ " missing" -> dne
-				| otherwise -> error $ "unknown response from git cat-file " ++ show (header, object)
+				| header == fromRef object ++ " missing" -> dne
+				| otherwise -> error $ "unknown response from git cat-file " ++ show (header, query)
 	readcontent objtype bytes from sha = do
 		content <- S.hGet from bytes
 		eatchar '\n' from
diff --git a/Git/Command.hs b/Git/Command.hs
--- a/Git/Command.hs
+++ b/Git/Command.hs
@@ -25,18 +25,10 @@
 gitCommandLine params r@(Repo { location = l@(Local _ _ ) }) =
 	setdir : settree ++ gitGlobalOpts r ++ params
   where
-	setdir = Param $ "--git-dir=" ++ gitpath (gitdir l)
+	setdir = Param $ "--git-dir=" ++ gitdir l
 	settree = case worktree l of
 		Nothing -> []
-		Just t -> [Param $ "--work-tree=" ++ gitpath t]
-#ifdef mingw32_HOST_OS
-	-- despite running on windows, msysgit wants a unix-formatted path
-	gitpath s
-		| absoluteGitPath s = "/" ++ dropDrive (toInternalGitPath s)
-		| otherwise = s
-#else
-	gitpath = id
-#endif
+		Just t -> [Param $ "--work-tree=" ++ t]
 gitCommandLine _ repo = assertLocal repo $ error "internal"
 
 {- Runs git in the specified repo. -}
diff --git a/Git/DiffTree.hs b/Git/DiffTree.hs
--- a/Git/DiffTree.hs
+++ b/Git/DiffTree.hs
@@ -36,12 +36,12 @@
 {- Diffs two tree Refs. -}
 diffTree :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
 diffTree src dst = getdiff (Param "diff-tree")
-	[Param (show src), Param (show dst)]
+	[Param (fromRef src), Param (fromRef dst)]
 
 {- Diffs two tree Refs, recursing into sub-trees -}
 diffTreeRecursive :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
 diffTreeRecursive src dst = getdiff (Param "diff-tree")
-	[Param "-r", Param (show src), Param (show dst)]
+	[Param "-r", Param (fromRef src), Param (fromRef dst)]
 
 {- Diffs between a tree and the index. Does nothing if there is not yet a
  - commit in the repository. -}
@@ -61,7 +61,7 @@
 diffIndex' ref params repo =
 	ifM (Git.Ref.headExists repo)
 		( getdiff (Param "diff-index")
-			( params ++ [Param $ show ref] )
+			( params ++ [Param $ fromRef ref] )
 			repo
 		, return ([], return True)
 		)
diff --git a/Git/Fsck.hs b/Git/Fsck.hs
--- a/Git/Fsck.hs
+++ b/Git/Fsck.hs
@@ -74,7 +74,7 @@
   where
 	dump = runQuiet
 		[ Param "show"
-		, Param (show s)
+		, Param (fromRef s)
 		] r
 
 findShas :: Bool -> String -> [Sha]
diff --git a/Git/HashObject.hs b/Git/HashObject.hs
--- a/Git/HashObject.hs
+++ b/Git/HashObject.hs
@@ -1,6 +1,6 @@
 {- git hash-object interface
  -
- - Copyright 2011-2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2011-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -13,6 +13,7 @@
 import Git.Command
 import Git.Types
 import qualified Utility.CoProcess as CoProcess
+import Utility.Tmp
 
 type HashObjectHandle = CoProcess.CoProcessHandle
 
@@ -34,7 +35,18 @@
 	send to = hPutStrLn to file
 	receive from = getSha "hash-object" $ hGetLine from
 
-{- Injects some content into git, returning its Sha. -}
+{- Injects a blob into git. Unfortunately, the current git-hash-object
+ - interface does not allow batch hashing without using temp files. -}
+hashBlob :: HashObjectHandle -> String -> IO Sha
+hashBlob h s = withTmpFile "hash" $ \tmp tmph -> do
+	hPutStr tmph s
+	hClose tmph
+	hashFile h tmp
+
+{- Injects some content into git, returning its Sha.
+ - 
+ - Avoids using a tmp file, but runs a new hash-object command each
+ - time called. -}
 hashObject :: ObjectType -> String -> Repo -> IO Sha
 hashObject objtype content = hashObject' objtype (flip hPutStr content)
 
diff --git a/Git/LsTree.hs b/Git/LsTree.hs
--- a/Git/LsTree.hs
+++ b/Git/LsTree.hs
@@ -38,13 +38,13 @@
 	<$> pipeNullSplitZombie (lsTreeParams t) repo
 
 lsTreeParams :: Ref -> [CommandParam]
-lsTreeParams t = [ Params "ls-tree --full-tree -z -r --", File $ show t ]
+lsTreeParams t = [ Params "ls-tree --full-tree -z -r --", File $ fromRef t ]
 
 {- Lists specified files in a tree. -}
 lsTreeFiles :: Ref -> [FilePath] -> Repo -> IO [TreeItem]
 lsTreeFiles t fs repo = map parseLsTree <$> pipeNullSplitStrict ps repo
   where
-  	ps = [Params "ls-tree --full-tree -z --", File $ show t] ++ map File fs
+  	ps = [Params "ls-tree --full-tree -z --", File $ fromRef t] ++ map File fs
 
 {- Parses a line of ls-tree output.
  - (The --long format is not currently supported.) -}
diff --git a/Git/Merge.hs b/Git/Merge.hs
--- a/Git/Merge.hs
+++ b/Git/Merge.hs
@@ -15,7 +15,7 @@
 {- Avoids recent git's interactive merge. -}
 mergeNonInteractive :: Ref -> Repo -> IO Bool
 mergeNonInteractive branch
-	| older "1.7.7.6" = merge [Param $ show branch]
-	| otherwise = merge [Param "--no-edit", Param $ show branch]
+	| older "1.7.7.6" = merge [Param $ fromRef branch]
+	| otherwise = merge [Param "--no-edit", Param $ fromRef branch]
   where
 	merge ps = runBool $ Param "merge" : ps
diff --git a/Git/Objects.hs b/Git/Objects.hs
--- a/Git/Objects.hs
+++ b/Git/Objects.hs
@@ -32,4 +32,4 @@
 looseObjectFile :: Repo -> Sha -> FilePath
 looseObjectFile r sha = objectsDir r </> prefix </> rest
   where
-	(prefix, rest) = splitAt 2 (show sha)
+	(prefix, rest) = splitAt 2 (fromRef sha)
diff --git a/Git/Ref.hs b/Git/Ref.hs
--- a/Git/Ref.hs
+++ b/Git/Ref.hs
@@ -20,12 +20,12 @@
 
 {- Converts a fully qualified git ref into a user-visible string. -}
 describe :: Ref -> String
-describe = show . base
+describe = fromRef . base
 
 {- Often git refs are fully qualified (eg: refs/heads/master).
  - Converts such a fully qualified ref into a base ref (eg: master). -}
 base :: Ref -> Ref
-base = Ref . remove "refs/heads/" . remove "refs/remotes/" . show
+base = Ref . remove "refs/heads/" . remove "refs/remotes/" . fromRef
   where
 	remove prefix s
 		| prefix `isPrefixOf` s = drop (length prefix) s
@@ -35,13 +35,13 @@
  - it under the directory. -}
 under :: String -> Ref -> Ref
 under dir r = Ref $ dir ++ "/" ++
-	(reverse $ takeWhile (/= '/') $ reverse $ show r)
+	(reverse $ takeWhile (/= '/') $ reverse $ fromRef r)
 
 {- Given a directory such as "refs/remotes/origin", and a ref such as
  - refs/heads/master, yields a version of that ref under the directory,
  - such as refs/remotes/origin/master. -}
 underBase :: String -> Ref -> Ref
-underBase dir r = Ref $ dir ++ "/" ++ show (base r)
+underBase dir r = Ref $ dir ++ "/" ++ fromRef (base r)
 
 {- A Ref that can be used to refer to a file in the repository, as staged
  - in the index.
@@ -64,12 +64,12 @@
 {- Checks if a ref exists. -}
 exists :: Ref -> Repo -> IO Bool
 exists ref = runBool
-	[Param "show-ref", Param "--verify", Param "-q", Param $ show ref]
+	[Param "show-ref", Param "--verify", Param "-q", Param $ fromRef ref]
 
 {- The file used to record a ref. (Git also stores some refs in a
  - packed-refs file.) -}
 file :: Ref -> Repo -> FilePath
-file ref repo = localGitDir repo </> show ref
+file ref repo = localGitDir repo </> fromRef ref
 
 {- Checks if HEAD exists. It generally will, except for in a repository
  - that was just created. -}
@@ -84,17 +84,17 @@
   where
 	showref = pipeReadStrict [Param "show-ref",
 		Param "--hash", -- get the hash
-		Param $ show branch]
+		Param $ fromRef branch]
 	process [] = Nothing
 	process s = Just $ Ref $ firstLine s
 
 {- List of (shas, branches) matching a given ref or refs. -}
 matching :: [Ref] -> Repo -> IO [(Sha, Branch)]
-matching refs repo =  matching' (map show refs) repo
+matching refs repo =  matching' (map fromRef refs) repo
 
 {- Includes HEAD in the output, if asked for it. -}
 matchingWithHEAD :: [Ref] -> Repo -> IO [(Sha, Branch)]
-matchingWithHEAD refs repo = matching' ("--head" : map show refs) repo
+matchingWithHEAD refs repo = matching' ("--head" : map fromRef refs) repo
 
 {- List of (shas, branches) matching a given ref or refs. -}
 matching' :: [String] -> Repo -> IO [(Sha, Branch)]
@@ -114,7 +114,7 @@
 {- Gets the sha of the tree a ref uses. -}
 tree :: Ref -> Repo -> IO (Maybe Sha)
 tree ref = extractSha <$$> pipeReadStrict
-	[ Param "rev-parse", Param (show ref ++ ":") ]
+	[ Param "rev-parse", Param (fromRef ref ++ ":") ]
 
 {- Checks if a String is a legal git ref name.
  -
diff --git a/Git/RefLog.hs b/Git/RefLog.hs
--- a/Git/RefLog.hs
+++ b/Git/RefLog.hs
@@ -18,5 +18,5 @@
 	[ Param "log"
 	, Param "-g"
 	, Param "--format=%H"
-	, Param (show b)
+	, Param (fromRef b)
 	]
diff --git a/Git/Repair.hs b/Git/Repair.hs
--- a/Git/Repair.hs
+++ b/Git/Repair.hs
@@ -168,7 +168,7 @@
 resetLocalBranches missing goodcommits r =
 	go [] [] goodcommits =<< filter islocalbranch <$> getAllRefs r
   where
-	islocalbranch b = "refs/heads/" `isPrefixOf` show b
+	islocalbranch b = "refs/heads/" `isPrefixOf` fromRef b
 	go changed deleted gcs [] = return (changed, deleted, gcs)
 	go changed deleted gcs (b:bs) = do
 		(mc, gcs') <- findUncorruptedCommit missing gcs b r
@@ -185,12 +185,12 @@
 		nukeBranchRef b	r
 		void $ runBool
 			[ Param "branch"
-			, Param (show $ Ref.base b)
-			, Param (show c)
+			, Param (fromRef $ Ref.base b)
+			, Param (fromRef c)
 			] r
 
 isTrackingBranch :: Ref -> Bool
-isTrackingBranch b = "refs/remotes/" `isPrefixOf` show b
+isTrackingBranch b = "refs/remotes/" `isPrefixOf` fromRef b
 
 {- To deal with missing objects that cannot be recovered, removes
  - any branches (filtered by a predicate) that reference them
@@ -231,10 +231,10 @@
 		nukeFile f
   where
 	makeref (sha, ref) = do
-		let dest = localGitDir r </> show ref
+		let dest = localGitDir r </> fromRef ref
 		createDirectoryIfMissing True (parentDir dest)
 		unlessM (doesFileExist dest) $
-			writeFile dest (show sha)
+			writeFile dest (fromRef sha)
 
 packedRefsFile :: Repo -> FilePath
 packedRefsFile r = localGitDir r </> "packed-refs"
@@ -249,7 +249,7 @@
 {- git-branch -d cannot be used to remove a branch that is directly
  - pointing to a corrupt commit. -}
 nukeBranchRef :: Branch -> Repo -> IO ()
-nukeBranchRef b r = nukeFile $ localGitDir r </> show b
+nukeBranchRef b r = nukeFile $ localGitDir r </> fromRef b
 
 {- Finds the most recent commit to a branch that does not need any
  - of the missing objects. If the input branch is good as-is, returns it.
@@ -268,7 +268,7 @@
 				[ Param "log"
 				, Param "-z"
 				, Param "--format=%H"
-				, Param (show branch)
+				, Param (fromRef branch)
 				] r
 			let branchshas = catMaybes $ map extractSha ls
 			reflogshas <- RefLog.get branch r
@@ -297,7 +297,7 @@
 			[ Param "log"
 			, Param "-z"
 			, Param "--format=%H %T"
-			, Param (show commit)
+			, Param (fromRef commit)
 			] r
 		let committrees = map parse ls
 		if any isNothing committrees || null committrees
@@ -501,9 +501,9 @@
 				, "remote tracking branches that referred to missing objects."
 				]
 		(resetbranches, deletedbranches, _) <- resetLocalBranches stillmissing goodcommits g
-		displayList (map show resetbranches)
+		displayList (map fromRef resetbranches)
 			"Reset these local branches to old versions before the missing objects were committed:"
-		displayList (map show deletedbranches)
+		displayList (map fromRef deletedbranches)
 			"Deleted these local branches, which could not be recovered due to missing objects:"
 		deindexedfiles <- rewriteIndex g
 		displayList deindexedfiles
@@ -519,7 +519,7 @@
 						Just curr -> when (any (== curr) modifiedbranches) $ do
 							putStrLn $ unwords
 								[ "You currently have"
-								, show curr
+								, fromRef curr
 								, "checked out. You may have staged changes in the index that can be committed to recover the lost state of this branch!"
 								]
 				putStrLn "Successfully recovered repository!"
diff --git a/Git/Sha.hs b/Git/Sha.hs
--- a/Git/Sha.hs
+++ b/Git/Sha.hs
@@ -37,3 +37,7 @@
 
 nullSha :: Ref		
 nullSha = Ref $ replicate shaSize '0'
+
+{- Git's magic empty tree. -}
+emptyTree :: Ref
+emptyTree = Ref "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
diff --git a/Git/Types.hs b/Git/Types.hs
--- a/Git/Types.hs
+++ b/Git/Types.hs
@@ -47,10 +47,10 @@
 
 {- A git ref. Can be a sha1, or a branch or tag name. -}
 newtype Ref = Ref String
-	deriving (Eq, Ord)
+	deriving (Eq, Ord, Read, Show)
 
-instance Show Ref where
-	show (Ref v) = v
+fromRef :: Ref -> String
+fromRef (Ref s) = s
 
 {- Aliases for Ref. -}
 type Branch = Ref
diff --git a/Git/UpdateIndex.hs b/Git/UpdateIndex.hs
--- a/Git/UpdateIndex.hs
+++ b/Git/UpdateIndex.hs
@@ -11,6 +11,9 @@
 	Streamer,
 	pureStreamer,
 	streamUpdateIndex,
+	streamUpdateIndex',
+	startUpdateIndex,
+	stopUpdateIndex,
 	lsTree,
 	updateIndexLine,
 	stageFile,
@@ -25,6 +28,9 @@
 import Git.FilePath
 import Git.Sha
 
+import Control.Exception (bracket)
+import System.Process (std_in)
+
 {- Streamers are passed a callback and should feed it lines in the form
  - read by update-index, and generated by ls-tree. -}
 type Streamer = (String -> IO ()) -> IO ()
@@ -35,17 +41,30 @@
 
 {- Streams content into update-index from a list of Streamers. -}
 streamUpdateIndex :: Repo -> [Streamer] -> IO ()
-streamUpdateIndex repo as = pipeWrite params repo $ \h -> do
+streamUpdateIndex repo as = bracket (startUpdateIndex repo) stopUpdateIndex $
+	(\h -> forM_ as $ streamUpdateIndex' h)
+
+data UpdateIndexHandle = UpdateIndexHandle ProcessHandle Handle
+
+streamUpdateIndex' :: UpdateIndexHandle -> Streamer -> IO ()
+streamUpdateIndex' (UpdateIndexHandle _ h) a = a $ \s -> do
+	hPutStr h s
+	hPutStr h "\0"
+
+startUpdateIndex :: Repo -> IO UpdateIndexHandle
+startUpdateIndex repo = do
+	(Just h, _, _, p) <- createProcess (gitCreateProcess params repo)
+		{ std_in = CreatePipe }
 	fileEncoding h
-	forM_ as (stream h)
-	hClose h
+	return $ UpdateIndexHandle p h
   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"
 
+stopUpdateIndex :: UpdateIndexHandle -> IO Bool
+stopUpdateIndex (UpdateIndexHandle p h) = do
+	hClose h
+	checkSuccessProcess p
+
 {- A streamer that adds the current tree for a ref. Useful for eg, copying
  - and modifying branches. -}
 lsTree :: Ref -> Repo -> Streamer
@@ -60,7 +79,7 @@
  - a given file with a given sha. -}
 updateIndexLine :: Sha -> BlobType -> TopFilePath -> String
 updateIndexLine sha filetype file =
-	show filetype ++ " blob " ++ show sha ++ "\t" ++ indexPath file
+	show filetype ++ " blob " ++ fromRef sha ++ "\t" ++ indexPath file
 
 stageFile :: Sha -> BlobType -> FilePath -> Repo -> IO Streamer
 stageFile sha filetype file repo = do
@@ -71,7 +90,7 @@
 unstageFile :: FilePath -> Repo -> IO Streamer
 unstageFile file repo = do
 	p <- toTopFilePath file repo
-	return $ pureStreamer $ "0 " ++ show nullSha ++ "\t" ++ indexPath p
+	return $ pureStreamer $ "0 " ++ fromRef nullSha ++ "\t" ++ indexPath p
 
 {- A streamer that adds a symlink to the index. -}
 stageSymlink :: FilePath -> Sha -> Repo -> IO Streamer
diff --git a/Limit.hs b/Limit.hs
--- a/Limit.hs
+++ b/Limit.hs
@@ -9,11 +9,6 @@
 
 module Limit where
 
-import Data.Time.Clock.POSIX
-import qualified Data.Set as S
-import qualified Data.Map as M
-import System.Path.WildMatch
-
 import Common.Annex
 import qualified Annex
 import qualified Utility.Matcher
@@ -28,6 +23,8 @@
 import Types.Group
 import Types.FileMatcher
 import Types.Limit
+import Types.MetaData
+import Logs.MetaData
 import Logs.Group
 import Logs.Unused
 import Logs.Location
@@ -35,15 +32,15 @@
 import Utility.HumanTime
 import Utility.DataUnits
 
+import Data.Time.Clock.POSIX
+import qualified Data.Set as S
+import qualified Data.Map as M
+import System.Path.WildMatch
+
 #ifdef WITH_TDFA
 import Text.Regex.TDFA
 import Text.Regex.TDFA.String
-#else
-#ifndef mingw32_HOST_OS
-import System.Path.WildMatch
-import Types.FileMatcher
 #endif
-#endif
 
 {- Checks if there are user-specified limits. -}
 limited :: Annex Bool
@@ -156,7 +153,7 @@
 limitInDir :: FilePath -> MkLimit
 limitInDir dir = const $ Right $ const go
   where
-	go (MatchingFile fi) = return $ any (== dir) $ splitPath $ takeDirectory $ matchFile fi
+	go (MatchingFile fi) = return $ elem dir $ splitPath $ takeDirectory $ matchFile fi
 	go (MatchingKey _) = return False
 
 {- Adds a limit to skip files not believed to have the specified number
@@ -266,6 +263,16 @@
 			fromIntegral . fileSize
 				<$> getFileStatus (relFile fi)
 		return $ filesize `vs` Just sz
+
+addMetaData :: String -> Annex ()
+addMetaData = addLimit . limitMetaData
+
+limitMetaData :: MkLimit
+limitMetaData s = case parseMetaData s of
+	Left e -> Left e
+	Right (f, v) -> Right $ const $ checkKey (check f v)
+  where
+  	check f v k = S.member v . metaDataValues f <$> getCurrentMetaData k
 
 addTimeLimit :: String -> Annex ()
 addTimeLimit s = do
diff --git a/Locations.hs b/Locations.hs
--- a/Locations.hs
+++ b/Locations.hs
@@ -40,6 +40,8 @@
 	gitAnnexJournalLock,
 	gitAnnexIndex,
 	gitAnnexIndexStatus,
+	gitAnnexViewIndex,
+	gitAnnexViewLog,
 	gitAnnexIgnoredRefs,
 	gitAnnexPidFile,
 	gitAnnexDaemonStatusFile,
@@ -252,6 +254,14 @@
 gitAnnexIndexStatus :: Git.Repo -> FilePath
 gitAnnexIndexStatus r = gitAnnexDir r </> "index.lck"
 
+{- The index file used to generate a filtered branch view._-}
+gitAnnexViewIndex :: Git.Repo -> FilePath
+gitAnnexViewIndex r = gitAnnexDir r </> "viewindex"
+
+{- File containing a log of recently accessed views. -}
+gitAnnexViewLog :: Git.Repo -> FilePath
+gitAnnexViewLog r = gitAnnexDir r </> "viewlog"
+
 {- List of refs that should not be merged into the git-annex branch. -}
 gitAnnexIgnoredRefs :: Git.Repo -> FilePath
 gitAnnexIgnoredRefs r = gitAnnexDir r </> "ignoredrefs"
@@ -330,7 +340,7 @@
 		-- other characters. By itself, it is escaped to 
 		-- doubled form.
 		| c == ',' = ",,"
-		| otherwise = ',' : show(ord(c))
+		| otherwise = ',' : show (ord c)
 
 {- Converts a key into a filename fragment without any directory.
  -
diff --git a/Logs.hs b/Logs.hs
--- a/Logs.hs
+++ b/Logs.hs
@@ -1,6 +1,6 @@
 {- git-annex log file names
  -
- - Copyright 2013 Joey Hess <joey@kitenet.net>
+ - Copyright 2013-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -15,7 +15,7 @@
 	= UUIDBasedLog
 	| NewUUIDBasedLog
 	| PresenceLog Key
-	| SingleValueLog
+	| OtherLog
 	deriving (Show)
 
 {- Converts a path from the git-annex branch into one of the varieties
@@ -24,7 +24,7 @@
 getLogVariety f
 	| f `elem` topLevelUUIDBasedLogs = Just UUIDBasedLog
 	| isRemoteStateLog f = Just NewUUIDBasedLog
-	| f == numcopiesLog = Just SingleValueLog
+	| isMetaDataLog f || f == numcopiesLog = Just OtherLog
 	| otherwise = PresenceLog <$> firstJust (presenceLogs f)
 
 {- All the uuid-based logs stored in the top of the git-annex branch. -}
@@ -119,14 +119,25 @@
 isRemoteStateLog :: FilePath -> Bool
 isRemoteStateLog path = remoteStateLogExt `isSuffixOf` path
 
+{- The filename of the metadata log for a given key. -}
+metaDataLogFile :: Key -> FilePath
+metaDataLogFile key = hashDirLower key </> keyFile key ++ metaDataLogExt
+
+metaDataLogExt :: String
+metaDataLogExt = ".log.met"
+
+isMetaDataLog :: FilePath -> Bool
+isMetaDataLog path = metaDataLogExt `isSuffixOf` path
+
 prop_logs_sane :: Key -> Bool
-prop_logs_sane dummykey = all id
+prop_logs_sane dummykey = and
 	[ isNothing (getLogVariety "unknown")
 	, expect isUUIDBasedLog (getLogVariety uuidLog)
 	, expect isPresenceLog (getLogVariety $ locationLogFile dummykey)
 	, expect isPresenceLog (getLogVariety $ urlLogFile dummykey)
 	, expect isNewUUIDBasedLog (getLogVariety $ remoteStateLogFile dummykey)
-	, expect isSingleValueLog (getLogVariety $ numcopiesLog)
+	, expect isOtherLog (getLogVariety $ metaDataLogFile dummykey)
+	, expect isOtherLog (getLogVariety $ numcopiesLog)
 	]
   where
   	expect = maybe False
@@ -136,5 +147,5 @@
 	isNewUUIDBasedLog _ = False
 	isPresenceLog (PresenceLog k) = k == dummykey
 	isPresenceLog _ = False
-	isSingleValueLog SingleValueLog = True
-	isSingleValueLog _ = False
+	isOtherLog OtherLog = True
+	isOtherLog _ = False
diff --git a/Logs/FsckResults.hs b/Logs/FsckResults.hs
--- a/Logs/FsckResults.hs
+++ b/Logs/FsckResults.hs
@@ -31,7 +31,7 @@
   	store s logfile = do 
 		createDirectoryIfMissing True (parentDir logfile)
 		liftIO $ viaTmp writeFile logfile $ serialize s
-	serialize = unlines . map show . S.toList
+	serialize = unlines . map fromRef . S.toList
 
 readFsckResults :: UUID -> Annex FsckResults
 readFsckResults u = do
diff --git a/Logs/MetaData.hs b/Logs/MetaData.hs
new file mode 100644
--- /dev/null
+++ b/Logs/MetaData.hs
@@ -0,0 +1,137 @@
+{- git-annex general metadata storage log
+ -
+ - A line of the log will look like "timestamp field [+-]value [...]"
+ -
+ - Note that unset values are preserved. Consider this case:
+ -
+ - We have:
+ -
+ - 100 foo +x
+ - 200 foo -x
+ -
+ - An unmerged remote has:
+ -
+ - 150 foo +x
+ - 
+ - After union merge, because the foo -x was preserved, we know that
+ - after the other remote redundantly set foo +x, it was unset,
+ - and so foo currently has no value.
+ -
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE CPP #-}
+
+module Logs.MetaData (
+	getCurrentMetaData,
+	getMetaData,
+	addMetaData,
+	addMetaData',
+	currentMetaData,
+) where
+
+import Common.Annex
+import Types.MetaData
+import qualified Annex.Branch
+import Logs
+import Logs.SingleValue
+
+import qualified Data.Set as S
+import Data.Time.Clock.POSIX
+
+instance SingleValueSerializable MetaData where
+	serialize = Types.MetaData.serialize
+	deserialize = Types.MetaData.deserialize
+
+getMetaData :: Key -> Annex (Log MetaData)
+getMetaData = readLog . metaDataLogFile
+
+{- Go through the log from oldest to newest, and combine it all
+ - into a single MetaData representing the current state. -}
+getCurrentMetaData :: Key -> Annex MetaData
+getCurrentMetaData = currentMetaData . collect <$$> getMetaData
+  where
+	collect = foldl' unionMetaData newMetaData . map value . S.toAscList
+
+{- Adds in some metadata, which can override existing values, or unset
+ - them, but otherwise leaves any existing metadata as-is. -}
+addMetaData :: Key -> MetaData -> Annex ()
+addMetaData k metadata = addMetaData' k metadata =<< liftIO getPOSIXTime
+
+{- Reusing the same timestamp when making changes to the metadata
+ - of multiple keys is a nice optimisation. The same metadata lines
+ - will tend to be generated across the different log files, and so
+ - git will be able to pack the data more efficiently. -}
+addMetaData' :: Key -> MetaData -> POSIXTime -> Annex ()
+addMetaData' k metadata now = Annex.Branch.change (metaDataLogFile k) $
+	showLog . simplifyLog 
+		. S.insert (LogEntry now metadata) 
+		. parseLog
+
+{- Simplify a log, removing historical values that are no longer
+ - needed. 
+ -
+ - This is not as simple as just making a single log line with the newest
+ - state of all metadata. Consider this case:
+ -
+ - We have:
+ -
+ - 100 foo +x bar +y
+ - 200 foo -x
+ -
+ - An unmerged remote has:
+ -
+ - 150 bar -y baz +w
+ -
+ - If what we have were simplified to "200 foo -x bar +y" then when the line
+ - from the remote became available, it would be older than the simplified
+ - line, and its change to bar would not take effect. That is wrong.
+ -
+ - Instead, simplify it to:
+ -
+ - 100 bar +y
+ - 200 foo -x
+ -
+ - (Note that this ends up with the same number of lines as the
+ - unsimplified version, so there's really no point in updating
+ - the log to this version. Doing so would only add data to git,
+ - with little benefit.)
+ -
+ - Now merging with the remote yields:
+ -
+ - 100 bar +y
+ - 150 bar -y baz +w
+ - 200 foo -x
+ -
+ - Simplifying again:
+ -
+ - 150 bar +z baz +w
+ - 200 foo -x
+ -}
+simplifyLog :: Log MetaData -> Log MetaData
+simplifyLog s = case sl of
+	(newest:rest) -> 
+		let sl' = go [newest] (value newest) rest
+		in if length sl' < length sl
+			then S.fromList sl'
+			else s
+	_ -> s
+  where
+#if MIN_VERSION_containers(0,5,0)
+	sl = S.toDescList s
+#else
+	sl = reverse (S.toAscList s)
+#endif
+
+	go c _ [] = c
+	go c newer (l:ls)
+		| unique == newMetaData = go c newer ls
+		| otherwise = go (l { value = unique } : c)
+			(unionMetaData unique newer) ls
+	  where
+		older = value l
+		unique = older `differenceMetaData` newer
diff --git a/Logs/Transfer.hs b/Logs/Transfer.hs
--- a/Logs/Transfer.hs
+++ b/Logs/Transfer.hs
@@ -17,6 +17,10 @@
 import Utility.Metered
 import Utility.Percentage
 import Utility.QuickCheck
+import Utility.PID
+#ifdef mingw32_HOST_OS
+import Utility.WinLock
+#endif
 
 import Data.Time.Clock
 import Data.Time.Clock.POSIX
@@ -24,20 +28,6 @@
 import System.Locale
 import Control.Concurrent
 
-#ifndef mingw32_HOST_OS
-import System.Posix.Types (ProcessID)
-#else
-import System.Win32.Process (ProcessId)
-import System.Win32.Process.Current (getCurrentProcessId)
-import Utility.WinLock
-#endif
-
-#ifndef mingw32_HOST_OS
-type PID = ProcessID
-#else
-type PID = ProcessId
-#endif
-
 {- Enough information to uniquely identify a transfer, used as the filename
  - of the transfer information file. -}
 data Transfer = Transfer
@@ -231,7 +221,7 @@
 #ifndef mingw32_HOST_OS
 	<*> pure Nothing -- pid not stored in file, so omitted for speed
 #else
-	<*> (Just <$> getCurrentProcessId)
+	<*> (Just <$> getPID)
 #endif
 	<*> pure Nothing -- tid ditto
 	<*> pure Nothing -- not 0; transfer may be resuming
diff --git a/Logs/View.hs b/Logs/View.hs
new file mode 100644
--- /dev/null
+++ b/Logs/View.hs
@@ -0,0 +1,89 @@
+{- git-annex recent views log
+ -
+ - The most recently accessed view comes first.
+ -
+ - This file is stored locally in .git/annex/, not in the git-annex branch.
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Logs.View (
+	currentView,
+	setView,
+	removeView,
+	recentViews,
+	branchView,
+	prop_branchView_legal,
+) where
+
+import Common.Annex
+import Types.View
+import Types.MetaData
+import qualified Git
+import qualified Git.Branch
+import qualified Git.Ref
+import Git.Types
+import Utility.Tmp
+
+import qualified Data.Set as S
+import Data.Char
+
+setView :: View -> Annex ()
+setView v = do
+	old <- take 99 . filter (/= v) <$> recentViews
+	writeViews (v : old)
+
+writeViews :: [View] -> Annex ()
+writeViews l = do
+	f <- fromRepo gitAnnexViewLog
+	liftIO $ viaTmp writeFile f $ unlines $ map show l
+
+removeView :: View -> Annex ()
+removeView v = writeViews =<< filter (/= v) <$> recentViews
+
+recentViews :: Annex [View]
+recentViews = do
+	f <- fromRepo gitAnnexViewLog
+	liftIO $ mapMaybe readish . lines <$> catchDefaultIO [] (readFile f)
+
+{- Gets the currently checked out view, if there is one. -}
+currentView :: Annex (Maybe View)
+currentView = go =<< inRepo Git.Branch.current
+  where
+	go (Just b) | branchViewPrefix `isPrefixOf` fromRef b =
+		headMaybe . filter (\v -> branchView v == b) <$> recentViews
+	go _ = return Nothing
+
+branchViewPrefix :: String
+branchViewPrefix = "refs/heads/views"
+
+{- Generates a git branch name for a View.
+ - 
+ - There is no guarantee that each view gets a unique branch name,
+ - but the branch name is used to express the view as well as possible.
+ -}
+branchView :: View -> Git.Branch
+branchView view
+	| null name = Git.Ref branchViewPrefix
+	| otherwise = Git.Ref $ branchViewPrefix ++ "/" ++ name
+  where
+	name = intercalate ";" $ map branchcomp (viewComponents view)
+	branchcomp c
+		| viewVisible c = branchcomp' c
+		| otherwise = "(" ++ branchcomp' c ++ ")"
+	branchcomp' (ViewComponent metafield viewfilter _) =concat
+		[ forcelegal (fromMetaField metafield)
+		, "="
+		, branchvals viewfilter
+		]
+	branchvals (FilterValues set) = intercalate "," $
+		map (forcelegal . fromMetaValue) $ S.toList set
+	branchvals (FilterGlob glob) = forcelegal glob
+	forcelegal s
+		| Git.Ref.legal True s = s
+		| otherwise = map (\c -> if isAlphaNum c then c else '_') s
+
+prop_branchView_legal :: View -> Bool
+prop_branchView_legal = Git.Ref.legal False . fromRef . branchView
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -252,6 +252,7 @@
 
 distributionupdate:
 	git pull
+	cabal configure
 	ghc --make Build/DistributionUpdate
 	./Build/DistributionUpdate
 
diff --git a/Remote.hs b/Remote.hs
--- a/Remote.hs
+++ b/Remote.hs
@@ -189,8 +189,7 @@
 remoteFromUUID :: UUID -> Annex (Maybe Remote)
 remoteFromUUID u = ifM ((==) u <$> getUUID)
 	( return Nothing
-	, do
-		maybe tryharder (return . Just) =<< findinmap
+	, maybe tryharder (return . Just) =<< findinmap
 	)
   where
 	findinmap = M.lookup u <$> remoteMap id
diff --git a/Remote/Bup.hs b/Remote/Bup.hs
--- a/Remote/Bup.hs
+++ b/Remote/Bup.hs
@@ -15,6 +15,7 @@
 import Common.Annex
 import Types.Remote
 import Types.Key
+import Types.Creds
 import qualified Git
 import qualified Git.Command
 import qualified Git.Config
@@ -82,8 +83,8 @@
   where
 	buprepo = fromMaybe (error "missing buprepo") $ remoteAnnexBupRepo gc
 
-bupSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-bupSetup mu c = do
+bupSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+bupSetup mu _ c = do
 	u <- maybe (liftIO genUUID) return mu
 
 	-- verify configuration is sane
diff --git a/Remote/Directory.hs b/Remote/Directory.hs
--- a/Remote/Directory.hs
+++ b/Remote/Directory.hs
@@ -16,6 +16,7 @@
 
 import Common.Annex
 import Types.Remote
+import Types.Creds
 import qualified Git
 import Config.Cost
 import Config
@@ -67,8 +68,8 @@
   where
 	dir = fromMaybe (error "missing directory") $ remoteAnnexDirectory gc
 
-directorySetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-directorySetup mu c = do
+directorySetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+directorySetup mu _ c = do
 	u <- maybe (liftIO genUUID) return mu
 	-- verify configuration is sane
 	let dir = fromMaybe (error "Specify directory=") $
diff --git a/Remote/External.hs b/Remote/External.hs
--- a/Remote/External.hs
+++ b/Remote/External.hs
@@ -73,8 +73,8 @@
   where
 	externaltype = fromMaybe (error "missing externaltype") (remoteAnnexExternalType gc)
 
-externalSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-externalSetup mu c = do
+externalSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+externalSetup mu _ c = do
 	u <- maybe (liftIO genUUID) return mu
 	let externaltype = fromMaybe (error "Specify externaltype=") $
 		M.lookup "externaltype" c
@@ -92,16 +92,18 @@
 
 store :: External -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
 store external k _f p = sendAnnex k rollback $ \f ->
-	storeHelper external k f p
+	metered (Just p) k $
+		storeHelper external k f
   where
 	rollback = void $ remove external k
 
 storeEncrypted :: External -> [CommandParam] -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
 storeEncrypted external gpgOpts (cipher, enck) k p = withTmp enck $ \tmp ->
 	sendAnnex k rollback $ \src -> do
-		liftIO $ encrypt gpgOpts cipher (feedFile src) $
-			readBytes $ L.writeFile tmp
-		storeHelper external enck tmp p
+		metered (Just p) k $ \meterupdate -> do
+			liftIO $ encrypt gpgOpts cipher (feedFile src) $
+				readBytes $ L.writeFile tmp
+			storeHelper external enck tmp meterupdate
   where
 	rollback = void $ remove external enck
 
@@ -118,17 +120,19 @@
 			_ -> Nothing
 
 retrieve :: External -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool
-retrieve external k _f d p = retrieveHelper external k d p
+retrieve external k _f d p = metered (Just p) k $
+	retrieveHelper external k d
 
 retrieveEncrypted :: External -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool
-retrieveEncrypted external (cipher, enck) _ f p = withTmp enck $ \tmp ->
-	ifM (retrieveHelper external enck tmp p)
-		( liftIO $ catchBoolIO $ do
-			decrypt cipher (feedFile tmp) $
-				readBytes $ L.writeFile f
-			return True
-		, return False
-		)
+retrieveEncrypted external (cipher, enck) k f p = withTmp enck $ \tmp ->
+	metered (Just p) k $ \meterupdate -> 
+		ifM (retrieveHelper external enck tmp meterupdate)
+			( liftIO $ catchBoolIO $ do
+				decrypt cipher (feedFile tmp) $
+					readBytes $ L.writeFile f
+				return True
+			, return False
+			)
 
 retrieveHelper :: External -> Key -> FilePath -> MeterUpdate -> Annex Bool
 retrieveHelper external k d p = safely $
@@ -221,8 +225,8 @@
 		send $ VALUE value
 	handleRemoteRequest (SETCREDS setting login password) = do
 		c <- liftIO $ atomically $ readTMVar $ externalConfig external
-		c' <- setRemoteCredPair' c (credstorage setting)
-			(login, password)
+		c' <- setRemoteCredPair c (credstorage setting) $
+			Just (login, password)
 		void $ liftIO $ atomically $ swapTMVar (externalConfig external) c'
 	handleRemoteRequest (GETCREDS setting) = do
 		c <- liftIO $ atomically $ readTMVar $ externalConfig external
diff --git a/Remote/GCrypt.hs b/Remote/GCrypt.hs
--- a/Remote/GCrypt.hs
+++ b/Remote/GCrypt.hs
@@ -21,6 +21,7 @@
 import Types.Remote
 import Types.GitConfig
 import Types.Crypto
+import Types.Creds
 import qualified Git
 import qualified Git.Command
 import qualified Git.Config
@@ -149,8 +150,8 @@
 unsupportedUrl :: Annex a
 unsupportedUrl = error "using non-ssh remote repo url with gcrypt is not supported"
 
-gCryptSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-gCryptSetup mu c = go $ M.lookup "gitrepo" c
+gCryptSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+gCryptSetup mu _ c = go $ M.lookup "gitrepo" c
   where
 	remotename = fromJust (M.lookup "name" c)
   	go Nothing = error "Specify gitrepo="
@@ -176,7 +177,7 @@
 		void $ inRepo $ Git.Command.runBool
 			[ Param "push"
 			, Param remotename
-			, Param $ show Annex.Branch.fullname
+			, Param $ Git.fromRef Annex.Branch.fullname
 			]
 		g <- inRepo Git.Config.reRead
 		case Git.GCrypt.remoteRepoId g (Just remotename) of
diff --git a/Remote/Glacier.hs b/Remote/Glacier.hs
--- a/Remote/Glacier.hs
+++ b/Remote/Glacier.hs
@@ -70,17 +70,18 @@
 			remotetype = remote
 		}
 
-glacierSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-glacierSetup mu c = do
+glacierSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+glacierSetup mu mcreds c = do
 	u <- maybe (liftIO genUUID) return mu
-	glacierSetup' u c
-glacierSetup' :: UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-glacierSetup' u c = do
+	glacierSetup' (isJust mu) u mcreds c
+glacierSetup' :: Bool -> UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+glacierSetup' enabling u mcreds c = do
 	c' <- encryptionSetup c
 	let fullconfig = c' `M.union` defaults
-	genVault fullconfig u
+	unless enabling $
+		genVault fullconfig u
 	gitConfigSpecialRemote u fullconfig "glacier" "true"
-	c'' <- setRemoteCredPair fullconfig (AWS.creds u)
+	c'' <- setRemoteCredPair fullconfig (AWS.creds u) mcreds
 	return (c'', u)
   where
 	remotename = fromJust (M.lookup "name" c)
@@ -245,7 +246,6 @@
   where
 	fileprefix = M.findWithDefault "" "fileprefix" $ config r
 
--- glacier vault create will succeed even if the vault already exists.
 genVault :: RemoteConfig -> UUID -> Annex ()
 genVault c u = unlessM (runGlacier c u params) $
 	error "Failed creating glacier vault."
diff --git a/Remote/Helper/AWS.hs b/Remote/Helper/AWS.hs
--- a/Remote/Helper/AWS.hs
+++ b/Remote/Helper/AWS.hs
@@ -22,9 +22,6 @@
 	, credPairRemoteKey = Just "s3creds"
 	}
 
-setCredsEnv :: CredPair -> IO ()
-setCredsEnv p = setEnvCredPair p $ creds undefined
-
 data Service = S3 | Glacier
 	deriving (Eq)
 
diff --git a/Remote/Hook.hs b/Remote/Hook.hs
--- a/Remote/Hook.hs
+++ b/Remote/Hook.hs
@@ -13,6 +13,7 @@
 import Common.Annex
 import Types.Remote
 import Types.Key
+import Types.Creds
 import qualified Git
 import Config
 import Config.Cost
@@ -65,8 +66,8 @@
   where
 	hooktype = fromMaybe (error "missing hooktype") $ remoteAnnexHookType gc
 
-hookSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-hookSetup mu c = do
+hookSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+hookSetup mu _ c = do
 	u <- maybe (liftIO genUUID) return mu
 	let hooktype = fromMaybe (error "Specify hooktype=") $
 		M.lookup "hooktype" c
diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs
--- a/Remote/Rsync.hs
+++ b/Remote/Rsync.hs
@@ -18,14 +18,6 @@
 	RsyncOpts
 ) where
 
-import qualified Data.ByteString.Lazy as L
-import qualified Data.Map as M
-#ifndef mingw32_HOST_OS
-import System.Posix.Process (getProcessID)
-#else
-import System.Win32.Process.Current (getCurrentProcessId)
-#endif
-
 import Common.Annex
 import Types.Remote
 import qualified Git
@@ -40,9 +32,14 @@
 import Utility.Rsync
 import Utility.CopyFile
 import Utility.Metered
+import Utility.PID
 import Annex.Perms
 import Logs.Transfer
+import Types.Creds
 
+import qualified Data.ByteString.Lazy as L
+import qualified Data.Map as M
+
 type RsyncUrl = String
 
 data RsyncOpts = RsyncOpts
@@ -138,8 +135,8 @@
 	loginopt = maybe [] (\l -> ["-l",l]) login
 	fromNull as xs = if null xs then as else xs
 
-rsyncSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-rsyncSetup mu c = do
+rsyncSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+rsyncSetup mu _ c = do
 	u <- maybe (liftIO genUUID) return mu
 	-- verify configuration is sane
 	let url = fromMaybe (error "Specify rsyncurl=") $
@@ -249,14 +246,10 @@
  - up trees for rsync. -}
 withRsyncScratchDir :: (FilePath -> Annex a) -> Annex a
 withRsyncScratchDir a = do
-#ifndef mingw32_HOST_OS
-	v <- liftIO getProcessID
-#else
-	v <- liftIO getCurrentProcessId
-#endif
+	p <- liftIO getPID
 	t <- fromRepo gitAnnexTmpDir
 	createAnnexDirectory t
-	let tmp = t </> "rsynctmp" </> show v
+	let tmp = t </> "rsynctmp" </> show p
 	nuke tmp
 	liftIO $ createDirectoryIfMissing True tmp
 	nuke tmp `after` a tmp
diff --git a/Remote/S3.hs b/Remote/S3.hs
--- a/Remote/S3.hs
+++ b/Remote/S3.hs
@@ -73,12 +73,12 @@
 			remotetype = remote
 		}
 
-s3Setup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-s3Setup mu c = do
+s3Setup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+s3Setup mu mcreds c = do
 	u <- maybe (liftIO genUUID) return mu
-	s3Setup' u c
-s3Setup' :: UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-s3Setup' u c = if isIA c then archiveorg else defaulthost
+	s3Setup' u mcreds c
+s3Setup' :: UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+s3Setup' u mcreds c = if isIA c then archiveorg else defaulthost
   where
 	remotename = fromJust (M.lookup "name" c)
 	defbucket = remotename ++ "-" ++ fromUUID u
@@ -92,7 +92,7 @@
 		
 	use fullconfig = do
 		gitConfigSpecialRemote u fullconfig "s3" "true"
-		c' <- setRemoteCredPair fullconfig (AWS.creds u)
+		c' <- setRemoteCredPair fullconfig (AWS.creds u) mcreds
 		return (c', u)
 
 	defaulthost = do
diff --git a/Remote/Tahoe.hs b/Remote/Tahoe.hs
--- a/Remote/Tahoe.hs
+++ b/Remote/Tahoe.hs
@@ -29,6 +29,7 @@
 
 import Common.Annex
 import Types.Remote
+import Types.Creds
 import qualified Git
 import Config
 import Config.Cost
@@ -85,8 +86,8 @@
 		remotetype = remote
 	}
 
-tahoeSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-tahoeSetup mu c = do
+tahoeSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+tahoeSetup mu _ c = do
 	furl <- fromMaybe (fromMaybe missingfurl $ M.lookup furlk c)
 		<$> liftIO (getEnv "TAHOE_FURL")
 	u <- maybe (liftIO genUUID) return mu
diff --git a/Remote/WebDAV.hs b/Remote/WebDAV.hs
--- a/Remote/WebDAV.hs
+++ b/Remote/WebDAV.hs
@@ -1,13 +1,13 @@
 {- WebDAV remotes.
  -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2012-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
 {-# LANGUAGE ScopedTypeVariables, CPP #-}
 
-module Remote.WebDAV (remote, davCreds, setCredsEnv, configUrl) where
+module Remote.WebDAV (remote, davCreds, configUrl) where
 
 import Network.Protocol.HTTP.DAV
 import qualified Data.Map as M
@@ -76,8 +76,8 @@
 			remotetype = remote
 		}
 
-webdavSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
-webdavSetup mu c = do
+webdavSetup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> Annex (RemoteConfig, UUID)
+webdavSetup mu mcreds c = do
 	u <- maybe (liftIO genUUID) return mu
 	let url = fromMaybe (error "Specify url=") $
 		M.lookup "url" c
@@ -85,7 +85,7 @@
 	creds <- getCreds c' u
 	testDav url creds
 	gitConfigSpecialRemote u c' "webdav" "true"
-	c'' <- setRemoteCredPair c' (davCreds u)
+	c'' <- setRemoteCredPair c' (davCreds u) mcreds
 	return (c'', u)
 
 store :: Remote -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
@@ -354,6 +354,3 @@
 	, credPairEnvironment = ("WEBDAV_USERNAME", "WEBDAV_PASSWORD")
 	, credPairRemoteKey = Just "davcreds"
 	}
-
-setCredsEnv :: (String, String) -> IO ()
-setCredsEnv creds = setEnvCredPair creds $ davCreds undefined
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -45,6 +45,7 @@
 import qualified Logs.Unused
 import qualified Logs.Transfer
 import qualified Logs.Presence
+import qualified Types.MetaData
 import qualified Remote
 import qualified Types.Key
 import qualified Types.Messages
@@ -53,6 +54,8 @@
 import qualified Crypto
 import qualified Annex.Init
 import qualified Annex.CatFile
+import qualified Annex.View
+import qualified Logs.View
 import qualified Utility.Path
 import qualified Utility.FileMode
 import qualified Build.SysConfig
@@ -144,12 +147,16 @@
 	, testProperty "prop_hashes_stable" Utility.Hash.prop_hashes_stable
 	, testProperty "prop_schedule_roundtrips" Utility.Scheduled.prop_schedule_roundtrips
 	, testProperty "prop_duration_roundtrips" Utility.HumanTime.prop_duration_roundtrips
+	, testProperty "prop_metadata_sane" Types.MetaData.prop_metadata_sane
+	, testProperty "prop_metadata_serialize" Types.MetaData.prop_metadata_serialize
+	, testProperty "prop_branchView_legal" Logs.View.prop_branchView_legal
+	, testProperty "prop_view_roundtrips" Annex.View.prop_view_roundtrips
 	]
 
 {- These tests set up the test environment, but also test some basic parts
  - of git-annex. They are always run before the unitTests. -}
 initTests :: TestEnv -> TestTree
-initTests env = testGroup ("Init Tests")
+initTests env = testGroup "Init Tests"
 	[ check "init" test_init
 	, check "add" test_add
 	]
@@ -230,7 +237,7 @@
 		( do
 			writeFile ingitfile $ content ingitfile
 			not <$> boolSystem "git" [Param "add", File ingitfile] @? "git add failed to fail in direct mode"
-			boolSystem "rm" [Params "-f", File ingitfile] @? "rm failed"
+			nukeFile ingitfile
 			git_annex env "sync" [] @? "sync failed"
 		, do
 			writeFile ingitfile $ content ingitfile
@@ -258,7 +265,7 @@
 test_reinject env = intmpclonerepoInDirect env $ do
 	git_annex env "drop" ["--force", sha1annexedfile] @? "drop failed"
 	writeFile tmp $ content sha1annexedfile
-	r <- annexeval $ Types.Backend.getKey backendSHA1 $
+	r <- annexeval $ Types.Backend.getKey backendSHA1
 		Types.KeySource.KeySource { Types.KeySource.keyFilename = tmp, Types.KeySource.contentLocation = tmp, Types.KeySource.inodeCache = Nothing }
 	let key = Types.Key.key2file $ fromJust r
 	git_annex env "reinject" [tmp, sha1annexedfile] @? "reinject failed"
@@ -542,7 +549,7 @@
 		git_annex env "fsck" [] @? "fsck unexpectedly failed again; previous one did not fix problem with " ++ f
 
 test_fsck_bare :: TestEnv -> Assertion
-test_fsck_bare env = intmpbareclonerepo env $ do
+test_fsck_bare env = intmpbareclonerepo env $
 	git_annex env "fsck" [] @? "fsck failed"
 
 test_fsck_localuntrusted :: TestEnv -> Assertion
@@ -585,7 +592,7 @@
 	annexed_present sha1annexedfile
 	if usegitattributes
 		then do
-			writeFile ".gitattributes" $ "* annex.backend=SHA1"
+			writeFile ".gitattributes" "* annex.backend=SHA1"
 			git_annex env "migrate" [sha1annexedfile]
 				@? "migrate sha1annexedfile failed"
 			git_annex env "migrate" [annexedfile]
@@ -601,7 +608,7 @@
 	checkbackend sha1annexedfile backendSHA1
 
 	-- check that reversing a migration works
-	writeFile ".gitattributes" $ "* annex.backend=SHA256"
+	writeFile ".gitattributes" "* annex.backend=SHA256"
 	git_annex env "migrate" [sha1annexedfile]
 		@? "migrate sha1annexedfile failed"
 	git_annex env "migrate" [annexedfile]
@@ -712,7 +719,7 @@
 	git_annex_expectoutput env "find" ["--exclude", "*"] []
 
 test_merge :: TestEnv -> Assertion
-test_merge env = intmpclonerepo env $ do
+test_merge env = intmpclonerepo env $
 	git_annex env "merge" [] @? "merge failed"
 
 test_info :: TestEnv -> Assertion
@@ -723,7 +730,7 @@
 		Text.JSON.Error e -> assertFailure e
 
 test_version :: TestEnv -> Assertion
-test_version env = intmpclonerepo env $ do
+test_version env = intmpclonerepo env $
 	git_annex env "version" [] @? "version failed"
 
 test_sync :: TestEnv -> Assertion
@@ -739,8 +746,8 @@
 test_union_merge_regression :: TestEnv -> Assertion
 test_union_merge_regression env =
 	{- We need 3 repos to see this bug. -}
-	withtmpclonerepo env False $ \r1 -> do
-		withtmpclonerepo env False $ \r2 -> do
+	withtmpclonerepo env False $ \r1 ->
+		withtmpclonerepo env False $ \r2 ->
 			withtmpclonerepo env False $ \r3 -> do
 				forM_ [r1, r2, r3] $ \r -> indir env r $ do
 					when (r /= r1) $
@@ -766,7 +773,7 @@
 {- Regression test for the automatic conflict resolution bug fixed
  - in f4ba19f2b8a76a1676da7bb5850baa40d9c388e2. -}
 test_conflict_resolution_movein_bug :: TestEnv -> Assertion
-test_conflict_resolution_movein_bug env = withtmpclonerepo env False $ \r1 -> do
+test_conflict_resolution_movein_bug env = withtmpclonerepo env False $ \r1 -> 
 	withtmpclonerepo env False $ \r2 -> do
 		let rname r = if r == r1 then "r1" else "r2"
 		forM_ [r1, r2] $ \r -> indir env r $ do
@@ -785,7 +792,7 @@
 				)
 		{- Sync twice in r1 so it gets the conflict resolution
 		 - update from r2 -}
-		forM_ [r1, r2, r1] $ \r -> indir env r $ do
+		forM_ [r1, r2, r1] $ \r -> indir env r $
 			git_annex env "sync" ["--force"] @? "sync failed in " ++ rname r
 		{- After the sync, it should be possible to get all
 		 - files. This includes both sides of the conflict,
@@ -935,7 +942,7 @@
 test_directory_remote :: TestEnv -> Assertion
 test_directory_remote env = intmpclonerepo env $ do
 	createDirectory "dir"
-	git_annex env "initremote" (words $ "foo type=directory encryption=none directory=dir") @? "initremote failed"
+	git_annex env "initremote" (words "foo type=directory encryption=none directory=dir") @? "initremote failed"
 	git_annex env "get" [annexedfile] @? "get of file failed"
 	annexed_present annexedfile
 	git_annex env "copy" [annexedfile, "--to", "foo"] @? "copy --to directory remote failed"
@@ -951,7 +958,7 @@
 test_rsync_remote env = intmpclonerepo env $ do
 #ifndef mingw32_HOST_OS
 	createDirectory "dir"
-	git_annex env "initremote" (words $ "foo type=rsync encryption=none rsyncurl=dir") @? "initremote failed"
+	git_annex env "initremote" (words "foo type=rsync encryption=none rsyncurl=dir") @? "initremote failed"
 	git_annex env "get" [annexedfile] @? "get of file failed"
 	annexed_present annexedfile
 	git_annex env "copy" [annexedfile, "--to", "foo"] @? "copy --to rsync remote failed"
@@ -1085,7 +1092,7 @@
 		Utility.Env.setEnv var val True
 
 	-- catch all errors, including normally fatal errors
-	r <- try (run)::IO (Either SomeException ())
+	r <- try run::IO (Either SomeException ())
 	case r of
 		Right _ -> return True
 		Left _ -> return False
@@ -1126,7 +1133,7 @@
 innewrepo env a = withgitrepo env $ \r -> indir env r a
 
 inmainrepo :: TestEnv -> Assertion -> Assertion
-inmainrepo env a = indir env mainrepodir a
+inmainrepo env = indir env mainrepodir
 
 intmpclonerepo :: TestEnv -> Assertion -> Assertion
 intmpclonerepo env a = withtmpclonerepo env False $ \r -> indir env r a
@@ -1163,7 +1170,7 @@
 	-- any type of error and change back to cwd before
 	-- rethrowing.
 	r <- bracket_ (changeToTmpDir env dir) (setCurrentDirectory cwd)
-		(try (a)::IO (Either SomeException ()))
+		(try a::IO (Either SomeException ()))
 	case r of
 		Right () -> return ()
 		Left e -> throw e
@@ -1186,7 +1193,7 @@
 	indir env new $
 		git_annex env "init" ["-q", new] @? "git annex init failed"
 	configrepo env new
-	when (not bare) $
+	unless bare $
 		indir env new $
 			handleforcedirect env
 	return new
@@ -1218,12 +1225,12 @@
 		mapM_ (void . tryIO . Utility.FileMode.allowWrite)
 	-- This sometimes fails on Windows, due to some files
 	-- being still opened by a subprocess.
-	catchIO (removeDirectoryRecursive dir) $ \e -> do
+	catchIO (removeDirectoryRecursive dir) $ \e ->
 		when final $ do
 			print e
 			putStrLn "sleeping 10 seconds and will retry directory cleanup"
 			Utility.ThreadScheduler.threadDelaySeconds (Utility.ThreadScheduler.Seconds 10)
-			whenM (doesDirectoryExist dir) $ do
+			whenM (doesDirectoryExist dir) $
 				removeDirectoryRecursive dir
 	
 checklink :: FilePath -> Assertion
@@ -1252,9 +1259,8 @@
 	-- modified despite permissions.
 	s <- getFileStatus f
 	let mode = fileMode s
-	if (mode == mode `unionFileModes` ownerWriteMode)
-		then assertFailure $ "able to modify annexed file's " ++ f ++ " content"
-		else return ()
+	when (mode == mode `unionFileModes` ownerWriteMode) $
+		assertFailure $ "able to modify annexed file's " ++ f ++ " content"
 
 checkwritable :: FilePath -> Assertion
 checkwritable f = do
@@ -1280,7 +1286,7 @@
 	case r of
 		Just (k, _) -> do
 			uuids <- annexeval $ Remote.keyLocations k
-			assertEqual ("bad content in location log for " ++ f ++ " key " ++ (Types.Key.key2file k) ++ " uuid " ++ show thisuuid)
+			assertEqual ("bad content in location log for " ++ f ++ " key " ++ Types.Key.key2file k ++ " uuid " ++ show thisuuid)
 				expected (thisuuid `elem` uuids)
 		_ -> assertFailure $ f ++ " failed to look up key"
 
@@ -1326,8 +1332,7 @@
 	release = releaseTestEnv
 
 releaseTestEnv :: TestEnv -> IO ()
-releaseTestEnv _env = do
-	cleanup' True tmpdir
+releaseTestEnv _env = cleanup' True tmpdir
 
 prepareTestEnv :: Bool -> IO TestEnv
 prepareTestEnv forcedirect = do
@@ -1404,7 +1409,7 @@
 changecontent f = writeFile f $ changedcontent f
 
 changedcontent :: FilePath -> String
-changedcontent f = (content f) ++ " (modified)"
+changedcontent f = content f ++ " (modified)"
 
 backendSHA1 :: Types.Backend
 backendSHA1 = backend_ "SHA1"
@@ -1416,4 +1421,4 @@
 backendWORM = backend_ "WORM"
 
 backend_ :: String -> Types.Backend
-backend_ name = Backend.lookupBackendName name
+backend_ = Backend.lookupBackendName
diff --git a/Types/Command.hs b/Types/Command.hs
--- a/Types/Command.hs
+++ b/Types/Command.hs
@@ -66,6 +66,7 @@
 	| SectionSetup
 	| SectionMaintenance
 	| SectionQuery
+	| SectionMetaData
 	| SectionUtility
 	| SectionPlumbing
 	deriving (Eq, Ord, Enum, Bounded)
@@ -75,5 +76,6 @@
 descSection SectionSetup = "Repository setup commands"
 descSection SectionMaintenance = "Repository maintenance commands"
 descSection SectionQuery = "Query commands"
+descSection SectionMetaData = "Metadata commands"
 descSection SectionUtility = "Utility commands"
 descSection SectionPlumbing = "Plumbing commands"
diff --git a/Types/Creds.hs b/Types/Creds.hs
new file mode 100644
--- /dev/null
+++ b/Types/Creds.hs
@@ -0,0 +1,12 @@
+{- credentials
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Types.Creds where
+
+type Creds = String -- can be any data that contains credentials
+
+type CredPair = (String, String) -- login, password
diff --git a/Types/MetaData.hs b/Types/MetaData.hs
new file mode 100644
--- /dev/null
+++ b/Types/MetaData.hs
@@ -0,0 +1,269 @@
+{- git-annex general metadata
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Types.MetaData (
+	MetaData(..),
+	MetaField(..),
+	MetaValue(..),
+	CurrentlySet(..),
+	serialize,
+	deserialize,
+	MetaSerializable,
+	toMetaField,
+	mkMetaField,
+	tagMetaField,
+	fromMetaField,
+	toMetaValue,
+	mkMetaValue,
+	unsetMetaValue,
+	unsetMetaData,
+	fromMetaValue,
+	fromMetaData,
+	newMetaData,
+	updateMetaData,
+	unionMetaData,
+	differenceMetaData,
+	isSet,
+	currentMetaData,
+	currentMetaDataValues,
+	metaDataValues,
+	ModMeta(..),
+	modMeta,
+	parseModMeta,
+	parseMetaData,
+	prop_metadata_sane,
+	prop_metadata_serialize
+) where
+
+import Common
+import Utility.Base64
+import Utility.QuickCheck
+
+import qualified Data.Set as S
+import qualified Data.Map as M
+import Data.Char
+
+newtype MetaData = MetaData (M.Map MetaField (S.Set MetaValue))
+	deriving (Show, Eq, Ord)
+
+{- A metadata value can be currently be set (True), or may have been
+ - set before and we're remembering it no longer is (False). -}
+newtype CurrentlySet = CurrentlySet Bool
+	deriving (Read, Show, Eq, Ord, Arbitrary)
+
+newtype MetaField = MetaField String
+	deriving (Read, Show, Eq, Ord)
+
+data MetaValue = MetaValue CurrentlySet String
+	deriving (Read, Show)
+
+{- Metadata values compare and order the same whether currently set or not. -}
+instance Eq MetaValue where
+	MetaValue _ a == MetaValue _ b = a == b
+instance Ord MetaValue where
+	compare (MetaValue _ x) (MetaValue _ y) = compare x y
+
+{- MetaData is serialized to a format like:
+ -
+ - field1 +val1 +val2 -val3 field2 +val4 +val5
+ -}
+class MetaSerializable v where
+	serialize :: v -> String
+	deserialize :: String -> Maybe v
+
+instance MetaSerializable MetaData where
+	serialize (MetaData m) = unwords $ concatMap go $ M.toList m
+	  where
+		go (f, vs) = serialize f : map serialize (S.toList vs)
+	deserialize = Just . getfield newMetaData . words
+	  where
+		getfield m [] = m
+		getfield m (w:ws) = maybe m (getvalues m ws) (deserialize w)
+		getvalues m [] _ = m
+		getvalues m l@(w:ws) f = case deserialize w of
+			Just v -> getvalues (updateMetaData f v m) ws f
+			Nothing -> getfield m l
+
+instance MetaSerializable MetaField where
+	serialize (MetaField f) = f
+	deserialize = Just . MetaField
+
+{- Base64 problimatic values. -}
+instance MetaSerializable MetaValue where
+	serialize (MetaValue isset v) =
+		serialize isset ++
+		if any isSpace v || "!" `isPrefixOf` v
+			then '!' : toB64 v
+			else v
+	deserialize (isset:'!':v) = MetaValue
+		<$> deserialize [isset]
+		<*> fromB64Maybe v
+	deserialize (isset:v) = MetaValue 
+		<$> deserialize [isset]
+		<*> pure v
+	deserialize [] = Nothing
+
+instance MetaSerializable CurrentlySet where
+	serialize (CurrentlySet True) = "+"
+	serialize (CurrentlySet False) = "-"
+	deserialize "+" = Just (CurrentlySet True)
+	deserialize "-" = Just (CurrentlySet False)
+	deserialize _ = Nothing
+
+{- Fields cannot be empty, contain whitespace, or start with "+-" as
+ - that would break the serialization. -}
+toMetaField :: String -> Maybe MetaField
+toMetaField f
+	| legalField f = Just $ MetaField f
+	| otherwise = Nothing
+
+legalField :: String -> Bool
+legalField f
+	| null f = False
+	| any isSpace f = False
+	| any (`isPrefixOf` f) ["+", "-"] = False
+	| otherwise = True
+
+toMetaValue :: String -> MetaValue
+toMetaValue = MetaValue (CurrentlySet True)
+
+mkMetaValue :: CurrentlySet -> String -> MetaValue
+mkMetaValue = MetaValue
+
+unsetMetaValue :: MetaValue -> MetaValue
+unsetMetaValue (MetaValue _ s) = MetaValue (CurrentlySet False) s
+
+{- Marks all MetaValues as no longer currently set. -}
+unsetMetaData :: MetaData -> MetaData
+unsetMetaData (MetaData m) = MetaData $ M.map (S.map unsetMetaValue) m
+
+fromMetaField :: MetaField -> String
+fromMetaField (MetaField f) = f
+
+fromMetaValue :: MetaValue -> String
+fromMetaValue (MetaValue _ f) = f
+
+fromMetaData :: MetaData -> [(MetaField, S.Set MetaValue)]
+fromMetaData (MetaData m) = M.toList m
+
+newMetaData :: MetaData
+newMetaData = MetaData M.empty
+
+{- Can be used to set a value, or to unset it, depending on whether
+ - the MetaValue has CurrentlySet or not. -}
+updateMetaData :: MetaField -> MetaValue -> MetaData -> MetaData
+updateMetaData f v (MetaData m) = MetaData $
+	M.insertWith' S.union f (S.singleton v) m
+
+{- New metadata overrides old._-}
+unionMetaData :: MetaData -> MetaData -> MetaData
+unionMetaData (MetaData old) (MetaData new) = MetaData $
+	M.unionWith S.union new old
+
+differenceMetaData :: MetaData -> MetaData -> MetaData
+differenceMetaData (MetaData m) (MetaData excludem) = MetaData $
+	M.differenceWith diff m excludem
+  where
+	diff sl sr =
+		let s = S.difference sl sr
+		in if S.null s then Nothing else Just s
+
+isSet :: MetaValue -> Bool
+isSet (MetaValue (CurrentlySet isset) _) = isset
+
+{- Gets only currently set values -}
+currentMetaDataValues :: MetaField -> MetaData -> S.Set MetaValue
+currentMetaDataValues f m = S.filter isSet (metaDataValues f m)
+
+currentMetaData :: MetaData -> MetaData
+currentMetaData (MetaData m) = removeEmptyFields $ MetaData $
+	M.map (S.filter isSet) m
+
+removeEmptyFields :: MetaData -> MetaData
+removeEmptyFields (MetaData m) = MetaData $ M.filter (not . S.null) m
+
+{- Gets currently set values, but also values that have been unset. -}
+metaDataValues :: MetaField -> MetaData -> S.Set MetaValue
+metaDataValues f (MetaData m) = fromMaybe S.empty (M.lookup f m)
+
+{- Ways that existing metadata can be modified -}
+data ModMeta
+	= AddMeta MetaField MetaValue
+	| DelMeta MetaField MetaValue
+	| SetMeta MetaField MetaValue -- removes any existing values
+
+{- Applies a ModMeta, generating the new MetaData.
+ - Note that the new MetaData does not include all the 
+ - values set in the input metadata. It only contains changed values. -}
+modMeta :: MetaData -> ModMeta -> MetaData
+modMeta _ (AddMeta f v) = updateMetaData f v newMetaData
+modMeta _ (DelMeta f oldv) = updateMetaData f (unsetMetaValue oldv) newMetaData
+modMeta m (SetMeta f v) = updateMetaData f v $
+	foldr (updateMetaData f) newMetaData $
+		map unsetMetaValue $ S.toList $ currentMetaDataValues f m
+
+{- Parses field=value, field+=value, field-=value -}
+parseModMeta :: String -> Either String ModMeta
+parseModMeta p = case lastMaybe f of
+	Just '+' -> AddMeta <$> mkMetaField f' <*> v
+	Just '-' -> DelMeta <$> mkMetaField f' <*> v
+	_ -> SetMeta <$> mkMetaField f <*> v
+  where
+	(f, sv) = separate (== '=') p
+	f' = beginning f
+	v = pure (toMetaValue sv)
+
+{- Parses field=value -}
+parseMetaData :: String -> Either String (MetaField, MetaValue)
+parseMetaData p = (,)
+	<$> mkMetaField f
+	<*> pure (toMetaValue v)
+  where
+	(f, v) = separate (== '=') p
+
+mkMetaField :: String -> Either String MetaField
+mkMetaField f = maybe (Left $ badField f) Right (toMetaField f)
+
+badField :: String -> String
+badField f = "Illegal metadata field name, \"" ++ f ++ "\""
+
+tagMetaField :: MetaField
+tagMetaField = MetaField "tag"
+
+{- Avoid putting too many fields in the map; extremely large maps make
+ - the seriaization test slow due to the sheer amount of data.
+ - It's unlikely that more than 100 fields of metadata will be used. -}
+instance Arbitrary MetaData where
+	arbitrary = do
+		size <- arbitrarySizedBoundedIntegral `suchThat` (< 500)
+		MetaData . M.fromList <$> vector size
+
+instance Arbitrary MetaValue where
+	arbitrary = MetaValue <$> arbitrary <*> arbitrary
+
+instance Arbitrary MetaField where
+	arbitrary = MetaField <$> arbitrary `suchThat` legalField 
+
+prop_metadata_sane :: MetaData -> MetaField -> MetaValue -> Bool
+prop_metadata_sane m f v = and
+	[ S.member v $ metaDataValues f m'
+	, not (isSet v) || S.member v (currentMetaDataValues f m')
+	, differenceMetaData m' newMetaData == m'
+	]
+  where
+	m' = updateMetaData f v m
+
+prop_metadata_serialize :: MetaField -> MetaValue -> MetaData -> Bool
+prop_metadata_serialize f v m = and
+	[ deserialize (serialize f) == Just f
+	, deserialize (serialize v) == Just v
+	, deserialize (serialize m') == Just m'
+	]
+  where
+  	m' = removeEmptyFields m
diff --git a/Types/Remote.hs b/Types/Remote.hs
--- a/Types/Remote.hs
+++ b/Types/Remote.hs
@@ -24,6 +24,7 @@
 import Types.UUID
 import Types.GitConfig
 import Types.Availability
+import Types.Creds
 import Config.Cost
 import Utility.Metered
 import Git.Types
@@ -41,7 +42,7 @@
 	-- generates a remote of this type
 	generate :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> a (Maybe (RemoteA a)),
 	-- initializes or changes a remote
-	setup :: Maybe UUID -> RemoteConfig -> a (RemoteConfig, UUID)
+	setup :: Maybe UUID -> Maybe CredPair -> RemoteConfig -> a (RemoteConfig, UUID)
 }
 
 instance Eq (RemoteTypeA a) where
diff --git a/Types/View.hs b/Types/View.hs
new file mode 100644
--- /dev/null
+++ b/Types/View.hs
@@ -0,0 +1,55 @@
+{- types for metadata based branch views
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Types.View where
+
+import Common.Annex
+import Types.MetaData
+import Utility.QuickCheck
+import qualified Git
+
+import qualified Data.Set as S
+
+{- A view is a list of fields with filters on their allowed values,
+ - which are applied to files in a parent git branch. -}
+data View = View
+	{ viewParentBranch :: Git.Branch
+	, viewComponents :: [ViewComponent]
+	}
+	deriving (Eq, Read, Show)
+
+instance Arbitrary View where
+	arbitrary = View <$> pure (Git.Ref "master") <*> arbitrary
+
+data ViewComponent = ViewComponent
+	{ viewField :: MetaField
+	, viewFilter :: ViewFilter
+	, viewVisible :: Bool
+	}
+	deriving (Eq, Read, Show)
+
+instance Arbitrary ViewComponent where
+	arbitrary = ViewComponent <$> arbitrary <*> arbitrary <*> arbitrary
+
+{- Only files with metadata matching the view are displayed. -}
+type FileView = FilePath
+type MkFileView = FilePath -> FileView
+
+data ViewFilter
+	= FilterValues (S.Set MetaValue)
+	| FilterGlob String
+	deriving (Eq, Read, Show)
+
+instance Arbitrary ViewFilter where
+	arbitrary = do
+		size <- arbitrarySizedBoundedIntegral `suchThat` (< 100)
+		FilterValues . S.fromList <$> vector size
+
+{- Can a ViewFilter match multiple different MetaValues? -}
+multiValue :: ViewFilter -> Bool
+multiValue (FilterValues s) = S.size s > 1
+multiValue (FilterGlob _) = True
diff --git a/Upgrade/V2.hs b/Upgrade/V2.hs
--- a/Upgrade/V2.hs
+++ b/Upgrade/V2.hs
@@ -106,7 +106,10 @@
 			showAction "pushing new git-annex branch to origin"
 			showOutput
 			inRepo $ Git.Command.run
-				[Param "push", Param "origin", Param $ show Annex.Branch.name]
+				[ Param "push"
+				, Param "origin"
+				, Param $ Git.fromRef Annex.Branch.name
+				]
 		_ -> do
 			-- no origin exists, so just let the user
 			-- know about the new branch
diff --git a/Utility/.Base64.hs.swp b/Utility/.Base64.hs.swp
new file mode 100644
Binary files /dev/null and b/Utility/.Base64.hs.swp differ
diff --git a/Utility/Daemon.hs b/Utility/Daemon.hs
--- a/Utility/Daemon.hs
+++ b/Utility/Daemon.hs
@@ -1,6 +1,6 @@
 {- daemon support
  -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2012-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -10,17 +10,20 @@
 module Utility.Daemon where
 
 import Common
+import Utility.PID
 #ifndef mingw32_HOST_OS
 import Utility.LogFile
+#else
+import Utility.WinProcess
+import Utility.WinLock
 #endif
 
 #ifndef mingw32_HOST_OS
 import System.Posix
 import Control.Concurrent.Async
-#else
-import System.PosixCompat.Types
 #endif
 
+#ifndef mingw32_HOST_OS
 {- Run an action as a daemon, with all output sent to a file descriptor.
  -
  - Can write its pid to a file, to guard against multiple instances
@@ -28,7 +31,6 @@
  -
  - When successful, does not return. -}
 daemonize :: Fd -> Maybe FilePath -> Bool -> IO () -> IO ()
-#ifndef mingw32_HOST_OS
 daemonize logfd pidfile changedirectory a = do
 	maybe noop checkalreadyrunning pidfile
 	_ <- forkProcess child1
@@ -52,18 +54,18 @@
 		wait =<< asyncWithUnmask (\unmask -> unmask a)
 		out
 	out = exitImmediately ExitSuccess
-#else
-daemonize = error "daemonize is not implemented on Windows" -- TODO
 #endif
 
-{- Locks the pid file, with an exclusive, non-blocking lock.
+{- Locks the pid file, with an exclusive, non-blocking lock,
+ - and leaves it locked on return.
+ -
  - Writes the pid to the file, fully atomically.
  - Fails if the pid file is already locked by another process. -}
 lockPidFile :: FilePath -> IO ()
-lockPidFile file = do
-	createDirectoryIfMissing True (parentDir file)
+lockPidFile pidfile = do
+	createDirectoryIfMissing True (parentDir pidfile)
 #ifndef mingw32_HOST_OS
-	fd <- openFd file ReadWrite (Just stdFileMode) defaultFileFlags
+	fd <- openFd pidfile ReadWrite (Just stdFileMode) defaultFileFlags
 	locked <- catchMaybeIO $ setLock fd (WriteLock, AbsoluteSeek, 0, 0)
 	fd' <- openFd newfile ReadWrite (Just stdFileMode) defaultFileFlags
 		{ trunc = True }
@@ -72,14 +74,21 @@
 		(Nothing, _) -> alreadyRunning
 		(_, Nothing) -> alreadyRunning
 		_ -> do
-			_ <- fdWrite fd' =<< show <$> getProcessID
+			_ <- fdWrite fd' =<< show <$> getPID
 			closeFd fd
+	rename newfile pidfile
+  where
+	newfile = pidfile ++ ".new"
 #else
-	writeFile newfile "-1"
+	{- Not atomic on Windows, oh well. -}
+	unlessM (isNothing <$> checkDaemon pidfile)
+		alreadyRunning
+	pid <- getPID
+	writeFile pidfile (show pid)
+	lckfile <- winLockFile pid pidfile
+	writeFile lckfile ""
+	void $ lockExclusive lckfile
 #endif
-	rename newfile file
-  where
-	newfile = file ++ ".new"
 
 alreadyRunning :: IO ()
 alreadyRunning = error "Daemon is already running."
@@ -88,7 +97,7 @@
  - is locked by the same process that is listed in the pid file.
  -
  - If it's running, returns its pid. -}
-checkDaemon :: FilePath -> IO (Maybe ProcessID)
+checkDaemon :: FilePath -> IO (Maybe PID)
 #ifndef mingw32_HOST_OS
 checkDaemon pidfile = do
 	v <- catchMaybeIO $
@@ -109,16 +118,44 @@
 			" (got " ++ show pid' ++ 
 			"; expected " ++ show pid ++ " )"
 #else
-checkDaemon pidfile = maybe Nothing readish <$> catchMaybeIO (readFile pidfile)
+checkDaemon pidfile = maybe (return Nothing) (check . readish)
+	=<< catchMaybeIO (readFile pidfile)
+  where
+	check Nothing = return Nothing
+	check (Just pid) = do
+		v <- lockShared =<< winLockFile pid pidfile
+		case v of
+			Just h -> do
+				dropLock h
+				return Nothing
+			Nothing -> return (Just pid)
 #endif
 
 {- Stops the daemon, safely. -}
 stopDaemon :: FilePath -> IO ()
-#ifndef mingw32_HOST_OS
 stopDaemon pidfile = go =<< checkDaemon pidfile
   where
 	go Nothing = noop
-	go (Just pid) = signalProcess sigTERM pid
+	go (Just pid) =
+#ifndef mingw32_HOST_OS
+		signalProcess sigTERM pid
 #else
-stopDaemon = error "stopDaemon is not implemented on Windows" -- TODO
+		terminatePID pid
+#endif
+
+{- Windows locks a lock file that corresponds with the pid of the process.
+ - This allows changing the process in the pid file and taking a new lock
+ - when eg, restarting the daemon.
+ -}
+#ifdef mingw32_HOST_OS
+winLockFile :: PID -> FilePath -> IO FilePath
+winLockFile pid pidfile = do
+	cleanstale
+	return $ prefix ++ show pid ++ suffix
+  where
+  	prefix = pidfile ++ "."
+	suffix = ".lck"
+	cleanstale = mapM_ (void . tryIO . removeFile) =<<
+		(filter iswinlockfile <$> dirContents (parentDir pidfile))
+	iswinlockfile f = suffix `isSuffixOf` f && prefix `isPrefixOf` f
 #endif
diff --git a/Utility/Directory.hs b/Utility/Directory.hs
--- a/Utility/Directory.hs
+++ b/Utility/Directory.hs
@@ -1,6 +1,6 @@
 {- directory manipulation
  -
- - Copyright 2011 Joey Hess <joey@kitenet.net>
+ - Copyright 2011-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -23,6 +23,7 @@
 import Utility.Tmp
 import Utility.Exception
 import Utility.Monad
+import Utility.Applicative
 
 dirCruft :: FilePath -> Bool
 dirCruft "." = True
@@ -72,6 +73,21 @@
 							, skip
 							)
 				_ -> skip
+
+{- Gets the directory tree from a point, recursively and lazily,
+ - with leaf directories **first**, skipping any whose basenames
+ - match the skipdir. Does not follow symlinks. -}
+dirTreeRecursiveSkipping :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
+dirTreeRecursiveSkipping skipdir topdir = go [] [topdir]
+  where
+  	go c [] = return c
+	go c (dir:dirs)
+		| skipdir (takeFileName dir) = go c dirs
+		| otherwise = unsafeInterleaveIO $ do
+			subdirs <- go c
+				=<< filterM (isDirectory <$$> getSymbolicLinkStatus)
+				=<< catchDefaultIO [] (dirContents dir)
+			go (subdirs++[dir]) dirs
 
 {- Moves one filename to another.
  - First tries a rename, but falls back to moving across devices if needed. -}
diff --git a/Utility/LogFile.hs b/Utility/LogFile.hs
--- a/Utility/LogFile.hs
+++ b/Utility/LogFile.hs
@@ -13,14 +13,12 @@
 
 import System.Posix.Types
 
-openLog :: FilePath -> IO Fd
 #ifndef mingw32_HOST_OS
+openLog :: FilePath -> IO Fd
 openLog logfile = do
 	rotateLog logfile
 	openFd logfile WriteOnly (Just stdFileMode)
 		defaultFileFlags { append = True }
-#else
-openLog = error "openLog TODO"
 #endif
 
 rotateLog :: FilePath -> IO ()
@@ -49,20 +47,14 @@
 maxLogs :: Int
 maxLogs = 9
 
-redirLog :: Fd -> IO ()
 #ifndef mingw32_HOST_OS
+redirLog :: Fd -> IO ()
 redirLog logfd = do
 	mapM_ (redir logfd) [stdOutput, stdError]
 	closeFd logfd
-#else
-redirLog _ = error "redirLog TODO"
-#endif
 
 redir :: Fd -> Fd -> IO ()
-#ifndef mingw32_HOST_OS
 redir newh h = do
 	closeFd h
 	void $ dupTo newh h
-#else
-redir _ _ = error "redir TODO"
 #endif
diff --git a/Utility/PID.hs b/Utility/PID.hs
new file mode 100644
--- /dev/null
+++ b/Utility/PID.hs
@@ -0,0 +1,31 @@
+{- process ids
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE CPP #-}
+
+module Utility.PID where
+
+#ifndef mingw32_HOST_OS
+import System.Posix.Types (ProcessID)
+import System.Posix.Process (getProcessID)
+#else
+import System.Win32.Process (ProcessId)
+import System.Win32.Process.Current (getCurrentProcessId)
+#endif
+
+#ifndef mingw32_HOST_OS
+type PID = ProcessID
+#else
+type PID = ProcessId
+#endif
+
+getPID :: IO PID
+#ifndef mingw32_HOST_OS
+getPID = getProcessID
+#else
+getPID = getCurrentProcessId
+#endif
diff --git a/Utility/Path.hs b/Utility/Path.hs
--- a/Utility/Path.hs
+++ b/Utility/Path.hs
@@ -277,3 +277,18 @@
 		| c == '.' = c
 		| isSpace c || isPunctuation c || isSymbol c || isControl c || c == '/' = '_'
 		| otherwise = c
+
+{- Similar to splitExtensions, but knows that some things in FilePaths
+ - after a dot are too long to be extensions. -}
+splitShortExtensions :: FilePath -> (FilePath, [String])
+splitShortExtensions = splitShortExtensions' 5 -- enough for ".jpeg"
+splitShortExtensions' :: Int -> FilePath -> (FilePath, [String])
+splitShortExtensions' maxextension = go []
+  where
+	go c f
+		| len > 0 && len <= maxextension && not (null base) = 
+			go (ext:c) base
+		| otherwise = (f, c)
+	  where
+		(base, ext) = splitExtension f
+		len = length ext
diff --git a/Utility/QuickCheck.hs b/Utility/QuickCheck.hs
--- a/Utility/QuickCheck.hs
+++ b/Utility/QuickCheck.hs
@@ -1,6 +1,6 @@
 {- QuickCheck with additional instances
  -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2012-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
@@ -17,10 +17,14 @@
 import Data.Time.Clock.POSIX
 import System.Posix.Types
 import qualified Data.Map as M
+import qualified Data.Set as S
 import Control.Applicative
 
 instance (Arbitrary k, Arbitrary v, Eq k, Ord k) => Arbitrary (M.Map k v) where
 	arbitrary = M.fromList <$> arbitrary
+
+instance (Arbitrary v, Eq v, Ord v) => Arbitrary (S.Set v) where
+	arbitrary = S.fromList <$> arbitrary
 
 {- Times before the epoch are excluded. -}
 instance Arbitrary POSIXTime where
diff --git a/Utility/WinProcess.hs b/Utility/WinProcess.hs
new file mode 100644
--- /dev/null
+++ b/Utility/WinProcess.hs
@@ -0,0 +1,19 @@
+{- Windows processes
+ -
+ - Copyright 2014 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module Utility.WinProcess where
+
+import Utility.PID
+
+import System.Win32.Process
+import Foreign.C
+import Control.Exception
+
+foreign import ccall unsafe "terminatepid"
+	terminatePID :: PID -> IO ()
diff --git a/Utility/winprocess.c b/Utility/winprocess.c
new file mode 100644
--- /dev/null
+++ b/Utility/winprocess.c
@@ -0,0 +1,10 @@
+#include <windows.h>
+
+void terminatepid (DWORD pid) {
+	HANDLE h;
+	h = OpenProcess(PROCESS_TERMINATE, 0, pid);
+	if (h != NULL) {
+		TerminateProcess(h, 1);
+	}
+	CloseHandle(h);
+}
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,34 @@
+git-annex (5.20140221) unstable; urgency=medium
+
+  * metadata: New command that can attach metadata to files.
+  * --metadata can be used to limit commands to acting on files
+    that have particular metadata.
+  * Preferred content expressions can use metadata=field=value
+    to limit them to acting on files that have particular metadata.
+  * view: New command that creates and checks out a branch that provides
+    a structured view of selected metadata.
+  * vfilter, vadd, vpop, vcycle: New commands for operating within views.
+  * pre-commit: Update metadata when committing changes to locations
+    of annexed files within a view.
+  * Add progress display for transfers to/from external special remotes.
+  * unused: Fix to actually detect unused keys when in direct mode.
+  * fsck: When run with --all or --unused, while .gitattributes
+    annex.numcopies cannot be honored since it's operating on keys
+    instead of files, make it honor the global numcopies setting,
+    and the annex.numcopies git config setting.
+  * trust, untrust, semitrust, dead: Warn when the trust level is
+    overridden in .git/config.
+  * glacier: Do not try to run glacier value create when an existing glacier
+    remote is enabled.
+  * fsck: Refuse to do anything if more than one of --incremental, --more,
+    and --incremental-schedule are given, since it's not clear which option
+    should win.
+  * Windows webapp: Can set up box.com, Amazon S3, and rsync.net remotes
+  * Windows webapp: Can create repos on removable drives.
+  * Windows: Ensure HOME is set, as needed by bundled cygwin utilities.
+
+ -- Joey Hess <joeyh@debian.org>  Fri, 21 Feb 2014 11:23:59 -0400
+
 git-annex (5.20140210) unstable; urgency=medium
 
   * --in can now refer to files that were located in a repository at
diff --git a/doc/assistant/release_notes.mdwn b/doc/assistant/release_notes.mdwn
--- a/doc/assistant/release_notes.mdwn
+++ b/doc/assistant/release_notes.mdwn
@@ -1,3 +1,10 @@
+## version 5.20140221
+
+The Windows port of the assistant and webapp is now considered to be beta
+quality. There are important missing features (notably Jabber), documented
+on [[todo/windows_support]], but the webapp is broadly usable on Windows
+now.
+
 ## version 5.20131221
 
 There is now a arm [[install/linux_standalone]] build of git-annex,
diff --git a/doc/bugs/Mac_OS_git_version_still_too_old_for_.gitignore__63__.mdwn b/doc/bugs/Mac_OS_git_version_still_too_old_for_.gitignore__63__.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Mac_OS_git_version_still_too_old_for_.gitignore__63__.mdwn
@@ -0,0 +1,28 @@
+### Please describe the problem.
+
+Joey, it looks like the git version wasn't updated with the latest release as is still too old to respect .gitignore files.  I'm hoping that I haven't just made a silly mistake but I don't think I have...
+
+See http://git-annex.branchable.com/bugs/Mac_OS_git_version_too_old_to_honour_.gitignore/ for bug that was closed.
+
+### What steps will reproduce the problem?
+
+Install git-annex 5.20140209-g3a61dbe and try to use .gitignore file to exclude items from git annex.
+
+### What version of git-annex are you using? On what operating system?
+
+5.20140209-g3a61dbe on Mac OS 10.9.1.
+
+### Please provide any additional information below.
+
+[/Applications/git-annex.app/Contents/MacOS]# ./git annex version
+
+git-annex version: 5.20140209-g3a61dbe
+build flags: Assistant Webapp Pairing S3 WebDAV FsEvents XMPP DNS Feeds Quvi TDFA CryptoHash
+key/value backends: SHA256E SHA1E SHA512E SHA224E SHA384E SKEIN256E SKEIN512E SHA256 SHA1 SHA512 SHA224 SHA384 SKEIN256 SKEIN512 WORM URL
+remote types: git gcrypt S3 bup directory rsync web webdav tahoe glacier hook external
+
+[/Applications/git-annex.app/Contents/MacOS]# ./git --version
+
+git version 1.8.3.4 (Apple Git-47)
+
+> Really [[fixed|done]] now. --[[Joey]]
diff --git a/doc/bugs/Matching_oddity_in_SafeCommand.hs.mdwn b/doc/bugs/Matching_oddity_in_SafeCommand.hs.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Matching_oddity_in_SafeCommand.hs.mdwn
@@ -0,0 +1,28 @@
+In SafeCommand.hs, the code to unwrap a File looks like:
+
+[[!format haskell """
+toCommand :: [CommandParam] -> [String]
+toCommand = concatMap unwrap
+  where
+        [...]
+        -- Files that start with a non-alphanumeric that is not a path
+        -- separator are modified to avoid the command interpreting them as
+        -- options or other special constructs.
+        unwrap (File s@(h:_))
+                | isAlphaNum h || h `elem` pathseps = [s]
+                | otherwise = ["./" ++ s]
+        unwrap (File s) = [s]
+        [...]
+"""]]
+
+I am not sure I understand which case would be caught in the last clause "unwrap (File s)". Is that the empty file? Because all non-empty file names seem to have been caught earlier, at least in the "otherwise" if they do not match the condition. In this case, wouldn't it be an error to use an empty file name and wouldn't it be better to throw an exception instead of returning [[]]?
+
+I would use:
+
+[[!format haskell """
+        unwrap (File []) = throw "Empty file name in SafeCommand.toCommand"
+"""]]
+
+or something similar instead.
+
+> [[done]]
diff --git a/doc/bugs/Numcopies_not_checked_when_running_with_--all.mdwn b/doc/bugs/Numcopies_not_checked_when_running_with_--all.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Numcopies_not_checked_when_running_with_--all.mdwn
@@ -0,0 +1,40 @@
+### Please describe the problem.
+There are a lot of differences in the behaviour of usual commands and commans using --all.
+The specific problem I found was that "git annex fsck --all" will only checksum it seems and not report back numcopies failures.
+Checking if objects/old versions have propagated is not possible without it or do I miss something.
+
+(As additional note not sure if related. It seems that git annex fsck --all is running much faster in my tests 1/3 faster. Any reason for that? Bug related?)
+
+
+### What steps will reproduce the problem?
+compare "git annex fsck" vs "git annex fsck" (no numcopies check)
+
+### What version of git-annex are you using? On what operating system?
+git-annex version: 5.20140210-gd99db49
+Linux (Ubuntu 13.10)
+
+### Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+
+# End of transcript or log.
+"""]]
+
+> It's expected that --all (and --unused) make .gitattributes
+> annex.numcopies settings be ignored, because with these options git-annex
+> is operating on keys, it does not know or care what filename they're
+> associated with, and so cannot look them up in .gitattributes. I have
+> improved the documentation of .gitattributes files to mention this
+> limitation.
+> 
+> I also notice that fsck --all is not checking .git/config's
+> annex.numcopies or the new global numcopies setting. It certianly makes
+> sense for those numcopies settings to be paid attention to.
+> [[fixed|done]] --[[Joey]]
+> 
+> (--all is faster because it can quickly scan through .git/annex/objects
+> to find everything, rather than looking at the symlink target of every
+> file in the work tree.)
diff --git a/doc/bugs/Resolve_.local_adresses_using_avahi_or_bonjour.mdwn b/doc/bugs/Resolve_.local_adresses_using_avahi_or_bonjour.mdwn
--- a/doc/bugs/Resolve_.local_adresses_using_avahi_or_bonjour.mdwn
+++ b/doc/bugs/Resolve_.local_adresses_using_avahi_or_bonjour.mdwn
@@ -13,4 +13,4 @@
 
 ### Please provide any additional information below.
 
-
+> [[closing|done]] --[[Joey]]
diff --git a/doc/bugs/assistant_unable_to_auth___40__windows__41__.mdwn b/doc/bugs/assistant_unable_to_auth___40__windows__41__.mdwn
--- a/doc/bugs/assistant_unable_to_auth___40__windows__41__.mdwn
+++ b/doc/bugs/assistant_unable_to_auth___40__windows__41__.mdwn
@@ -81,3 +81,5 @@
   -q       --quiet         avoid verbose output
 etc
 """]]
+
+> [[fixed|done]]; both for regular remote ssh servers, and for rsync.net --[[Joey]] 
diff --git a/doc/bugs/assistant_using_the_incorrect_path_on_windows__63__.mdwn b/doc/bugs/assistant_using_the_incorrect_path_on_windows__63__.mdwn
--- a/doc/bugs/assistant_using_the_incorrect_path_on_windows__63__.mdwn
+++ b/doc/bugs/assistant_using_the_incorrect_path_on_windows__63__.mdwn
@@ -40,3 +40,6 @@
 
 ### What version of git-annex are you using? On what operating system?
 Windows 7, git-annex version 5.20131230-g192d991
+
+> [[fixed|done]]; git-annex now ensures HOME is set when running cygwin
+> commands that require it. --[[Joey]]
diff --git a/doc/bugs/can__39__t_drop_unused_files_that_never_were_added.mdwn b/doc/bugs/can__39__t_drop_unused_files_that_never_were_added.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/can__39__t_drop_unused_files_that_never_were_added.mdwn
@@ -0,0 +1,86 @@
+### Please describe the problem.
+
+When adding files to the annex and then deciding against it in an "unusual" way, git-annex gets confused and the file left behind can't be removed from the annex...
+
+### What steps will reproduce the problem?
+
+1. Add file with "git annex add"
+2. Decide you don't need the file add all
+3. "git rm -f newfile"
+4. "git annex unused"
+5. "git annex dropunused all"
+
+### What version of git-annex are you using? On what operating system?
+
+git-annex version: 5.20140210 on Debian unstable
+
+### Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+$ git init
+Initialized empty Git repository in /tmp/foo/.git/
+$ ls -l
+total 0
+$ cp ~/download/hub-ctrl.c .
+$ git add hub-ctrl.c
+$ git commit
+[master (root-commit) ed7eb68] A file.
+ 1 file changed, 412 insertions(+)
+ create mode 100644 hub-ctrl.c
+$ cp ~/download/hub-ctrl .
+$ ls -l
+total 28
+-rwxr-xr-x 1 tobias tobias 14130 Feb 19 00:49 hub-ctrl
+-rw-r--r-- 1 tobias tobias  9270 Feb 19 00:48 hub-ctrl.c
+$ git annex init
+init  ok
+(Recording state in git...)
+$ git annex add
+add hub-ctrl ok
+(Recording state in git...)
+$ git status
+On branch master
+Changes to be committed:
+  (use "git reset HEAD <file>..." to unstage)
+
+	new file:   hub-ctrl
+
+$ git rm hub-ctrl
+error: the following file has changes staged in the index:
+    hub-ctrl
+(use --cached to keep the file, or -f to force removal)
+$ git rm -f hub-ctrl
+rm 'hub-ctrl'
+$ git status
+On branch master
+nothing to commit, working directory clean
+$ git annex unused
+unused . (checking for unused data...) (checking HEAD...) 
+  Some annexed data is no longer used by any files:
+    NUMBER  KEY
+    1       SHA256E-s14130--d4e777ba2b99ed0a520fbabe7b93cf2165373b4945afe8dcb626231d9051f19d
+  (To see where data was previously used, try: git log --stat -S'KEY')
+  
+  To remove unwanted data: git-annex dropunused NUMBER
+  
+ok
+$ git annex dropunused all
+dropunused 1 (unsafe) 
+  Could only verify the existence of 0 out of 1 necessary copies
+
+  Rather than dropping this file, try using: git annex move
+
+  (Use --force to override this check, or adjust numcopies.)
+failed
+git-annex: dropunused: 1 failed
+$
+
+# End of transcript or log.
+"""]]
+
+> It seems to me that if you run `git annex dropunused --force`, it will
+> remove the file. This needing --force is a recent change; git-annex
+> tries to never posibly lose data unless forced. Dropping the last
+> copy of a file certianly qualifies. [[done]] --[[Joey]]
diff --git a/doc/bugs/copy_unused_and_unused_not_agreeing.mdwn b/doc/bugs/copy_unused_and_unused_not_agreeing.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/copy_unused_and_unused_not_agreeing.mdwn
@@ -0,0 +1,48 @@
+[[!format sh """
+greg@x200s:~/Documents$ git-annex unused
+unused . (checking for unused data...) (checking annex/direct/master...) (checking synced/annex/direct/master...) (checking synology/master...) 
+  Some annexed data is no longer used by any files:
+    NUMBER  KEY
+    1       SHA256E-s16367--0b00ef0154c42a0bf94e5be8d92d8af27455d59794f26c33dfc39c09178521c9.pdf
+    2       SHA256E-s84--b08e1c831863bb43c02158bd5e8f3f5416c3a5203d89fa94a22149460142c273.odt
+    3       SHA256E-s84--ec4caae451180a29721f2b6667eec8ec80eaa724f0727cf99d2bb21bf9218e9d.odt
+    ...
+    88      SHA256E-s84--710d69bef61674b04974ac550d713e5928563b2a12b902b64fe451705b967452.doc
+    89      SHA256E-s3830--1348d6248e35625da3e22f73d2a0017185bb5e1aa37f65bbca5dfcb3c7f53034
+    90      SHA256E-s119822--7c1b53ab6402b8835473f0b5c326f3cc300ac9372be79694942c1efa4bcdc621.pdf
+    91      SHA256E-s84--63b6188696795885ff6570a76a3a74799396787f7058cbcfd4a2c40b22982420.odt
+  (To see where data was previously used, try: git log --stat -S'KEY')
+  
+  To remove unwanted data: git-annex dropunused NUMBER
+  
+ok
+greg@x200s:~/Documents$ git-annex copy --unused --to synology
+"""]]
+
+Is that correct behavior? I would assume the last command would at least run through the 91 files and check my synology remote that they are there. Like this repo did:
+
+[[!format sh """
+$ git-annex unused
+unused . (checking for unused data...) (checking master...) (checking 60justin/master...)
+  Some annexed data is no longer used by any files:
+    NUMBER  KEY
+    1       SHA256E-s9390266--7ed16c9423b331dbe63bb3b4278b8c94a6754a07177c53fceb3b24e9610e8054.NEF
+    2       SHA256E-s10435713--49cbfe8466eada2c3787c9a7e158a7dfb9845a0aa8ef862ed2578b59c889dc4d.NEF
+    3       SHA256E-s9442044--85c314e318f643237df5e3adf7559e9bf268ee28f1f92d4102161865323ddeb6.NEF
+    4       SHA256E-s290672--c5822c3ef16bd62b5752b2dace81182ce00d64bd4d2d994ba93e3cb94e645708.JPG
+    5       SHA256E-s293288--30f1367fc326f7b053012818863151206f9e3ddeab3c3fc5b5c1c573d120d50a.JPG
+    6       SHA256E-s3672986--be960f6dc247df2496f634f7d788bd4a180fe556230e2dafc23ebc8fc1f10af3.JPG
+  (To see where data was previously used, try: git log --stat -S'KEY')
+
+  To remove unwanted data: git-annex dropunused NUMBER
+
+ok
+$ git-annex copy --unused --to synology
+copy SHA256E-s9390266--7ed16c9423b331dbe63bb3b4278b8c94a6754a07177c53fceb3b24e9610e8054.NEF (checking synology...) ok
+copy SHA256E-s10435713--49cbfe8466eada2c3787c9a7e158a7dfb9845a0aa8ef862ed2578b59c889dc4d.NEF (checking synology...) ok
+copy SHA256E-s9442044--85c314e318f643237df5e3adf7559e9bf268ee28f1f92d4102161865323ddeb6.NEF (checking synology...) ok
+copy SHA256E-s290672--c5822c3ef16bd62b5752b2dace81182ce00d64bd4d2d994ba93e3cb94e645708.JPG (checking synology...) ok
+copy SHA256E-s293288--30f1367fc326f7b053012818863151206f9e3ddeab3c3fc5b5c1c573d120d50a.JPG (checking synology...) ok
+copy SHA256E-s3672986--be960f6dc247df2496f634f7d788bd4a180fe556230e2dafc23ebc8fc1f10af3.JPG (checking synology...) ok
+$
+"""]]
diff --git a/doc/bugs/fsck_giving_false_checking_information.mdwn b/doc/bugs/fsck_giving_false_checking_information.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/fsck_giving_false_checking_information.mdwn
@@ -0,0 +1,29 @@
+### Please describe the problem.
+When a repository has no object of a given file and git annex fsck is run it still shows "fsck file ok", which is missleading in the sense, that it gives the impression that it checked the file is alright/checksummed.
+
+As a result of this it seems that incremental fscks are not incremental with non checkable objects. On each run (after the first one) with "git annex fsck --incremental --more --schedule-limit 1d" all files without objects are checked even so it should wait another day till it checks again.
+
+Probably best to say checksum couldn't be checked on x files (only give that as quiet output, not every check)
+
+Another thing, which came up as a problem was, that checksum fsck would not be wanted to run as often as numcopie checks.
+When the incremental fsck is used to check for bad files "git annex fsck --incremental --more --limit 1m" after a fast numcopies check "git annex fsck --incremental --more --limit 1m fast" it messes up the actual bad files check.
+As both are currently using the same incremental "lock"-file they are colliding.
+
+### What steps will reproduce the problem?
+-
+
+### What version of git-annex are you using? On what operating system?
+git-annex version: 5.20140210-gd99db49
+Linux (Ubuntu 13.10)
+
+### Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+
+# End of transcript or log.
+"""]]
+
+> [[fixed|done]] --[[Joey]]
diff --git a/doc/bugs/git_annex_sync_--content_not_syncing_all_objects.mdwn b/doc/bugs/git_annex_sync_--content_not_syncing_all_objects.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/git_annex_sync_--content_not_syncing_all_objects.mdwn
@@ -0,0 +1,31 @@
+### Please describe the problem.
+When "git annex sync --content" is used only objects currently in the working tree are synced. It doesn't honor full archives, which should get all objects.
+So only objects similar to "git annex find --want-get" are synced and not every available object "git annex get --all"
+
+
+### What steps will reproduce the problem?
+# mein repo:
+git annex add file
+git annex sync
+git annex rm file
+git annex sync
+
+# other repo:
+git annex wanted here "not copies=backup:3"
+git annex find --want-get # will be empty
+git annex sync --content # nothing is synced even so all files should be backed up
+git annex get --all # will sync the object from file
+
+### What version of git-annex are you using? On what operating system?
+git-annex version: 5.20140210-gd99db49
+Linux (Ubuntu 13.10)
+
+### Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+
+# End of transcript or log.
+"""]]
diff --git a/doc/bugs/git_repo_fails_to_checkout.mdwn b/doc/bugs/git_repo_fails_to_checkout.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/git_repo_fails_to_checkout.mdwn
@@ -0,0 +1,39 @@
+### Please describe the problem.
+
+Attempting to clone the git repository produces
+
+    (master) cayley:git-annex% git checkout -f HEAD
+    error: unable to create file doc/bugs/fatal:_unable_to_access___39__..__47__..__47__..__47__..__47__C:__92__Users__92____91__...__93____92__annex__92__.git__47__config__39__:_Invalid_argument___40__Windows__41__.mdwn (File name too long)
+    fatal: cannot create directory at 'doc/bugs/fatal:_unable_to_access___39__..__47__..__47__..__47__..__47__C:__92__Users__92____91__...__93____92__annex__92__.git__47__config__39__:_Invalid_argument___40__Windows__41__': File name too long
+
+### What steps will reproduce the problem?
+
+I get the above with either
+
+    git clone https://github.com/joeyh/git-annex
+
+or (after this fails) retrying with
+
+    cd git-annex
+    git checkout -f HEAD
+
+### What version of git-annex are you using? On what operating system?
+
+I am running git 1.9.0 from git (5f95c9f850b19b368c43ae399cc831b17a26a5ac in the git git repo) on Ubuntu 13.04.
+
+> More encfs brain-damange.
+
+	One such limitation is filename length.  If your underlying
+	filesystem limits you to N characters in a filename, then
+	EncFS will limit you to approximately 3*(N-2)/4.
+ 
+> It's really astounding that Ubuntu inflicts that POS on users.
+> However, I can't see that as justification for limiting the
+> git-annex repository to filenames shorter than `PATH_MAX` -- just
+> as DOS's problems with both filename length and also `:` in filenames
+> is not a good reason to mangle the repository.
+> 
+> In either case, it's up to the user to find a way to make it work. 
+> In the DOS case, that involves using Cygwin's git. In the encfs case,
+> it presumably means checking it out into a real filesystem.
+> [[done]] --[[Joey]] 
diff --git a/doc/bugs/numcopies_deprecated__44___but_still_in_walkthrough.mdwn b/doc/bugs/numcopies_deprecated__44___but_still_in_walkthrough.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/numcopies_deprecated__44___but_still_in_walkthrough.mdwn
@@ -0,0 +1,21 @@
+### Please describe the problem.
+Within the walkthrough the deprecated annex.numcopies are still used. At least it should be added that "git annex numcopies x" can be used to define it globally without the need for a .gitattribute file. And use .gitattribute only for per directory and fine grained control.
+http://git-annex.branchable.com/walkthrough/backups/
+
+### What steps will reproduce the problem?
+
+
+### What version of git-annex are you using? On what operating system?
+
+
+### Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+
+# End of transcript or log.
+"""]]
+
+> [[fixed|done]] --[[Joey]]
diff --git a/doc/bugs/problems_with_android_and_gpg.mdwn b/doc/bugs/problems_with_android_and_gpg.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/problems_with_android_and_gpg.mdwn
@@ -0,0 +1,73 @@
+### Please describe the problem.
+When my android phone tries to decode files downloaded from a ssh remote using shared encryption, gpg errors occur.
+
+### What steps will reproduce the problem?
+Setup is very similar to my other bug report in <https://git-annex.branchable.com/bugs/problems_with_android_and_xmpp/>.
+Only difference is the location of the annex on the android side.
+I have put it on the /data mount which uses ext4 to avoid the /sdcard fuse mount, which does not handle symlinks.
+
+1) setup git annex via webapp on laptop:
+
+* local annex
+
+* remote annex via ssh with shared encryption (tried two different servers, one with and one without git-annex installed)
+
+* share with my devices using jabber.me account
+
+2) setup git annex via webapp on android:
+
+* local annex in /data/data/ga.androidterm/annex (ext filesystem)
+
+* share with my devices using jabber.me account
+
+* ssh remote is automatically added via XMPP
+
+3) add file to annex on linux, which gets uploaded to the ssh remote
+
+4) symlink for file is created on phone and data downloaded, but never decrypted (see logs below)
+
+### What version of git-annex are you using? On what operating system?
+Ubunut Linux 12.04 with git-annex version:
+
+* 5.20140127.1 (from PPA)
+
+Android 4.2 (rooted) with git-annex version:
+
+* 5.20140211-g556cfeb (from autobuilds)
+
+### Please provide any additional information below.
+full logs:
+
+* linux: <http://pastebin.ca/2639929>
+
+* android: <http://pastebin.ca/2639945>
+
+most interesting parts:
+[[!format sh """
+#
+# android:
+#
+gpg: can't open `/usr/local/share/gnupg/options.skel': No such file or directory
+gpg: DBG: locking for `/sdcard/git-annex.home/.gnupg/secring.gpg.lock' done via O_EXCL
+gpg: DBG: locking for `/sdcard/git-annex.home/.gnupg/pubring.gpg.lock' done via O_EXCL
+gpg: decryption failed: bad key
+
+# followed by just the last line for all further attemps
+gpg: decryption failed: bad key
+
+# gpg it self seems to be fine
+root@android:/data/data/ga.androidterm # ./bin/gpg --version -v                
+gpg (GnuPG) 1.4.15
+Copyright (C) 2013 Free Software Foundation, Inc.
+License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
+This is free software: you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
+
+Home: ~/.gnupg
+Supported algorithms:
+Pubkey: RSA, RSA-E, RSA-S, ELG-E, DSA
+Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
+        CAMELLIA128, CAMELLIA192, CAMELLIA256
+Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
+Compression: Uncompressed, ZIP, ZLIB
+"""]]
diff --git a/doc/bugs/problems_with_android_and_xmpp.mdwn b/doc/bugs/problems_with_android_and_xmpp.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/problems_with_android_and_xmpp.mdwn
@@ -0,0 +1,82 @@
+### Please describe the problem.
+When trying to sync my android phone with my linux laptop using the git annex assistant and XMPP no files are transferred.
+
+### What steps will reproduce the problem?
+1) setup git annex via webapp on laptop:
+
+* local annex
+
+* remote annex via ssh with shared encryption (tried two different servers, one with and one without git-annex installed)
+
+* share with my devices using jabber.me account
+
+2) setup git annex via webapp on android:
+
+* local annex in /sdcard/annex (fuse filesystem without symlink support)
+
+* share with my devices using jabber.me account
+
+* ssh remote is automatically added via XMPP
+
+3) add files to annex on linux, they get uploaded to the ssh remote
+
+4) wait forever for any files from linux to download to phone
+
+5) add files to annex on phone, they get uploaded to the ssh remote
+
+4) wait forever for any files from phone to download to linux
+
+### What version of git-annex are you using? On what operating system?
+Ubunut Linux 12.04 with git-annex version:
+
+* 5.20140127.1 (from PPA)
+
+Android 4.2 (rooted) tried with git-annex versions:
+
+* 5.20140209 (from http://downloads.kitenet.net/git-annex/android/current/4.0/)
+
+* 5.20140211-g556cfeb (from autobuilds)
+
+### Please provide any additional information below.
+full logs:
+
+(these do not show the uploads to the ssh remote, because I restarted to get clean and short logs, but the files are on the server and can be dropped and restored on the linux side manually) 
+
+* linux: <http://pastebin.ca/2639948>
+
+* android: <http://pastebin.ca/2639952>
+
+most interesting parts:
+[[!format sh """
+#
+# linux:
+#
+
+[2014-02-13 13:11:27 CET] XMPPClient: Pairing with dorian in progress
+[2014-02-13 13:11:28 CET] XMPPSendPack: Syncing with dorian 
+To xmpp::dorian@jabber.me
+ * [new branch]      git-annex -> refs/synced/4ce7576f-6f02-4657-bab5-2f4c4a564ee7/ZG9yaWFuQGphYmJlci5tZQ==/git-annex
+ * [new branch]      annex/direct/master -> refs/synced/4ce7576f-6f02-4657-bab5-2f4c4a564ee7/ZG9yaWFuQGphYmJlci5tZQ==/annex/direct/master
+[2014-02-13 13:11:29 CET] XMPPSendPack: Syncing with dorian 
+Everything up-to-date
+[2014-02-13 13:12:21 CET] XMPPSendPack: Syncing with dorian 
+Everything up-to-date
+[2014-02-13 13:12:21 CET] XMPPSendPack: Syncing with dorian 
+fatal: Could not read from remote repository.
+
+Please make sure you have the correct access rights
+and the repository exists.
+
+#
+# android:
+#
+[2014-02-13 13:16:25 CET] XMPPClient: sending: Pushing "d29" (ReceivePackOutput 2 "<elided>")
+[2014-02-13 13:16:25 CET] XMPPClient: to client: d6/tigase-14134
+[2014-02-13 13:18:22 CET] XMPPClient: received: ["Unknown message"]
+[2014-02-13 13:18:25 CET] XMPPReceivePack: timeout waiting for git send-pack output via XMPP
+fatal: The remote end hung up unexpectedly
+[2014-02-13 13:18:25 CET] XMPPReceivePack: finished running push Pushing "d29" (StartingPush (UUID "4ce7576f-6f02-4657-bab5-2f4c4a564ee7")) True
+[2014-02-13 13:18:25 CET] XMPPClient: sending: Pushing "d29" (ReceivePackDone (ExitFailure 128))
+[2014-02-13 13:18:25 CET] XMPPClient: to client: d6/tigase-14134
+
+"""]]
diff --git a/doc/bugs/rsync_transport:_username_not_respected.mdwn b/doc/bugs/rsync_transport:_username_not_respected.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/rsync_transport:_username_not_respected.mdwn
@@ -0,0 +1,38 @@
+### Please describe the problem.
+
+I created an encrypted rsync remote with a username (user@host). The rsync command issued by git-annex doesn't contain the username. I solved the problem using .ssh/config.
+
+### What steps will reproduce the problem?
+
+
+[[!format sh """
+# Add remote like that
+git-annex initremote encrsync type=rsync rsyncurl=user@xxx.rsync.net:rsync/X keyid=0xXXX
+# Sync it
+git-annex sync --content
+
+
+# You'll see
+$> ps ax | grep rsync
+30652 pts/3    S+     0:00 /home/ganwell/bin/git-annex.linux//lib64/ld-linux-x86-64.so.2 --library-path /home/ganwell/bin/git-annex.linux//etc/ld.so.conf.d:/home/ganwell/bin/git-annex.linux//usr/lib/x86_64-linux-gnu/gconv:/home/ganwell/bin/git-annex.linux//usr/lib/x86_64-linux-gnu/libc:/home/ganwell/bin/git-annex.linux//usr/lib:/home/ganwell/bin/git-annex.linux//usr/lib/x86_64-linux-gnu:/home/ganwell/bin/git-annex.linux//lib64:/home/ganwell/bin/git-annex.linux//lib/x86_64-linux-gnu: /home/ganwell/bin/git-annex.linux/shimmed/rsync/rsync xxx.rsync.net:rsync/X/9fa/634/'GPGHMACSHA1--X/GPGHMACSHA1--X
+"""]]
+
+
+
+### What version of git-annex are you using? On what operating system?
+
+OS: Linux
+
+Ver: git-annex version: 5.20140210-g1e0a3ad
+
+Type: prebuilt
+
+### Please provide any additional information below.
+
+[[!format sh """
+# If you can, paste a complete transcript of the problem occurring here.
+# If the problem is with the git-annex assistant, paste in .git/annex/daemon.log
+
+
+# End of transcript or log.
+"""]]
diff --git a/doc/bugs/trust_command_and_gitconfig_contradiction_causing_confusion.mdwn b/doc/bugs/trust_command_and_gitconfig_contradiction_causing_confusion.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/trust_command_and_gitconfig_contradiction_causing_confusion.mdwn
@@ -0,0 +1,35 @@
+### Please describe the problem.
+'trust', 'dead', etc commands completing with 'ok' but fail to acknowledge contradiction in .git/config, causing confusion.
+
+[[!format sh """
+greg@x200s:~/Photos$ git-annex untrust home  # yes, bad remote name
+untrust home ok
+(Recording state in git...) 
+greg@x200s:~/Photos$ git-annex status  # /me is old-school and forgets
+greg@x200s:~/Photos$ git-annex info
+repository mode: indirect
+trusted repositories: 2
+	c0e4106e-2631-11e2-9749-1bfa37a61069 -- rose
+ 	c69d6fcc-18d1-11e2-9487-2fe6dbf0516b -- home (photos on eeepc)
+
+....
+
+greg@x200s:~/Photos$ git-annex dead home
+dead home ok
+(Recording state in git...)
+greg@x200s:~/Photos$ git-annex info
+repository mode: indirect
+trusted repositories: 2
+	c0e4106e-2631-11e2-9749-1bfa37a61069 -- rose
+ 	c69d6fcc-18d1-11e2-9487-2fe6dbf0516b -- home (photos on eeepc)
+"""]]
+
+The home remote has "annex-trustlevel=trusted" in .git/config
+
+
+Maybe have those commands instead say "Hey, this is different than what you said explicitly in .git/config, ya sure? (y/n)" If y, overwrite config, if n, abort.
+
+### What version of git-annex are you using? On what operating system?
+5.20140127 on Debian
+
+> [[Fixed|done]], it will now warn about this situation. --[[Joey]]
diff --git a/doc/design.mdwn b/doc/design.mdwn
--- a/doc/design.mdwn
+++ b/doc/design.mdwn
@@ -1,8 +1,6 @@
 git-annex's high-level design is mostly inherent in the data that it
 stores in git, and alongside git. See [[internals]] for details.
 
-See [[encryption]] for design of encryption elements.
-
-See [[roadmap]] for planning and scheduling of new stuff.
+Here's the other design documents we have:
 
-See [[assistant]] for the design site for the git-annex [[/assistant]].
+[[!map pages="page(design/*) and !design/*/*"]]
diff --git a/doc/design/assistant/telehash.mdwn b/doc/design/assistant/telehash.mdwn
--- a/doc/design/assistant/telehash.mdwn
+++ b/doc/design/assistant/telehash.mdwn
@@ -9,6 +9,8 @@
   telehash v1 (even that seems incomplete). I have pinged its author
   to see if he anticipates updating it.
 * Rapid development, situation may change in a month or 2.
+* Is it secure? A security review should be done by competant people
+  (not Joey). See <https://github.com/telehash/telehash.org/issues/23>
 
 ## implementation basics
 
diff --git a/doc/design/external_special_remote_protocol.mdwn b/doc/design/external_special_remote_protocol.mdwn
--- a/doc/design/external_special_remote_protocol.mdwn
+++ b/doc/design/external_special_remote_protocol.mdwn
@@ -188,7 +188,7 @@
   This is always the same for any given Key, so can be used for eg,
   creating hash directory structures to store Keys in.  
   (git-annex replies with VALUE followed by the value.)
-* `SETCONFIG Setting`  
+* `SETCONFIG Setting Value`  
   Sets one of the special remote's configuration settings.  
   Normally this is sent during INITREMOTE, which allows these settings
   to be stored in the git-annex branch, so will be available if the same
diff --git a/doc/design/metadata.mdwn b/doc/design/metadata.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/metadata.mdwn
@@ -0,0 +1,209 @@
+[[!toc]]
+
+# metadata
+
+Attach an arbitrary set of metadata to a key. This consists of any number
+of fields. Each field has an unordered set of values. The special field
+"tag" has as its values any tags that are set for the key.
+
+Store in git-annex branch, next to location log files.
+
+Storage needs to support union merging, including removing an old value
+of a field, and adding a new value of a field.
+
+# filtered branches
+
+See [[tips/metadata_driven_views]]
+
+The reason to use specially named filtered branches is because it makes
+self-documenting how the repository is currently filtered.
+
+TODO: Files not matching the view should be able to be included.
+For example, it could make a "unsorted" directory containing files
+without a tag when viewing by tag. If also viewing by author, the unsorted
+directories nest.
+
+## operations while on filtered branch
+
+* If files are removed and git commit called, git-annex should remove the
+  relevant metadata from the files.  
+  TODO: It's not clear that
+  removing a file should nuke all the metadata used to filter it into the
+  branch (especially if it's derived metadata like the year).
+  Currently, only metadata used for visible subdirs is added and removed
+  this way.
+  Also, this is not usable in direct mode because deleting the
+  file.. actually deletes it.
+* If a file is moved into a new subdirectory while in a view branch,
+  a tag is added with the subdir name. This allows on the fly tagging.
+  **done**
+* `git annex sync` should avoid pushing out the view branch, but
+  it should check if there are changes to the metadata pulled in, and update
+  the branch to reflect them.
+* TODO: If `git annex add` adds a file, it gets all the metadata of the filter
+  branch it's added to. If it's in a relevent directory (like fosdem-2014),
+  it gets that metadata automatically recorded as well.
+
+## automatically added metadata
+
+TODO git annex add should automatically attach the current mtime of a file
+when adding it.
+
+Could also automatically attach permissions.
+
+TODO A git hook could be run by git annex add to gather more metadata.
+For example, by examining MP3 metadata.
+
+Also auto add metadata when adding files to view branches. See below.
+
+## derived metadata
+
+This is probably not stored anywhere. It's computed on demand by a pure
+function from the other metadata. 
+(Should be a general mechanism for this. (It probably generalizes to
+sql queries if we want to go that far.))
+
+### data metadata
+
+TODO From the ctime, some additional 
+metadata is derived, at least year=yyyy and probably also month, etc.
+
+### directory hierarchy metadata
+
+TODO From the original filename used in the master branch, when
+constructing a view, generate fields. For example foo/bar/baz.mp3
+would get /=foo, foo/=bar, foo/bar/=baz, and .=mp3.
+
+Note that dir/=subdir allows a view to use `dir/=*` and only
+match one level of subdirs with the glob. So is better than dir=foo/bar
+as the metadata. (Alternatively, could do special glob matching.)
+
+This allows using whatever directory hierarchy exists to inform the view,
+without locking the view into using it. 
+
+Complication: When refining a view, it only looks at the filenames in
+the view, so it would need to map from
+those filenames to derive the same metadata, unless there is persistent
+storage. Luckily, the filenames used in the views currently include the
+subdirs (although not quite in a parseable format, would need some small
+changes).
+
+# other uses for metadata
+
+Uses are not limited to view branches.
+
+`git annex checkoutmeta year=2014 talk` in a subdir of master could create the
+same tree of files filter would. The user can then commit that if desired.
+Or, they could run additional commands like `git annex fadd` to refine the
+tree of files in the subdir.
+
+Metadata can be used for configuring numcopies. One way would be a
+numcopies=n value attached to a file. But perhaps better would be to make
+the numcopies.log allow configuring numcopies based on which files have
+other metadata.
+
+Other programs could query git-annex for the metadata of files in the work
+tree, and do whatever it wants with it.
+
+# filenames
+
+The hard part of this is actually getting a useful filename to put in the
+view branch, since git-annex only has a key which the user will not
+want to see.
+
+* Could use filename metadata for the key, recorded by git-annex add (which
+  may not correspond to filenames being used in regular git branches like
+  master for the key).
+* Could use the .map files to get a filename, but this is somewhat
+  arbitrary (.map can contain multiple filenames), and is only
+  currently supported in direct mode.
+* Current approach: Have a reference branch (eg master) and walk it to
+  find filenames and
+  keys. Fine as long as it can be done efficiently. Also allows including
+  the subdirectory a file is in, potentially. cwebber points out that this
+  is essentially a form of tracking branch. Which implies it will need to
+  be updatable when the reference branch changes. Should be doable via
+  diff-tree.
+
+Note that we have to take care to avoid generating conflicting filenames.
+The current approach is to embed the full directory structure inside the
+filename in the view branch.
+
+## union merge properties
+
+While the storage could just list all the current values of a field on a
+line with a timestamp, that's not good enough. Two disconnected
+repositories can make changes to the values of a field (setting and
+unsetting tags for example) and when this is union merged back together,
+the changes need to be able to be replayed in order to determine which
+values we end up with. 
+
+To make that work, we log not only when a field is set to a value, 
+but when a value is unset as well.
+
+For example, here two different remotes added tags, and then later
+a tag was removed:
+
+	1287290776.765152s tag +foo +bar
+	1287290991.152124s tag +baz
+	1291237510.141453s tag -bar
+
+# efficient metadata lookup
+
+Looking up metadata for view generation so far requires traversing all keys
+in the git-annex branch. This is slow. A fast cache is needed.
+
+TODO
+
+# direct mode issues
+
+TODO (direct mode is currently not supported with view branches)
+
+Checking out a view branch can result in any number of copies of a file
+appearing in different directories. No problem in indirect mode, but
+in direct mode these are real, expensive copies.
+
+But, it's worth supporting direct mode!
+
+So, possible approaches:
+
+* Before checking out a view branch, calculate how much space will
+  be used by duplicates and refuse if not enough is free.
+* Only check out one file, and omit the copies. Keep track of which
+  files were omitted, and make sure that when committing on the branch,
+  that metadata is not removed. Has the downside that files can seem
+  to randomly move around in the tree as their metadata changes.
+* Disallow view branch checkouts that have duplicate files.
+  This would cripple it some, but perhaps not too badly?
+
+# gotchas
+
+* Checking out a view branch can remove the current subdir. May be worth
+  detecting when this happens and help the user.
+  **done**
+
+* Git has a complex set of rules for what is legal in a ref name.
+  View branch names will need to filter out any illegal stuff. **done**
+
+* Filesystems that are not case sensative (including case preserving OSX)
+  will cause problems if view branches try to use different cases for 
+  2 directories representing the value of some metadata. But, users
+  probably want at least case-preserving metadata values. 
+  
+  Solution might be to compare metadata case-insensitively, and
+  pick one representation consistently, so if, for example an author
+  field uses mixed case, it will be used in the view branch.
+
+  Alternatively, it could escape `A` to `_A` when such a filesystem
+  is detected and avoid collisions that way (double `_` to escape it).
+  This latter option is ugly, but so are non-posix filesystems.. and it
+  also solves any similar issues with case-colliding filenames.
+
+  TODO: Check current state of this.
+
+* Assistant needs to know about views, so it can update metadata when
+  files are moved around inside them. TODO
+
+* What happens if git annex add or the assistant add a new file while on a
+  view? If the file is not also added to the master branch, it will be lost
+  when exiting the view. TODO
diff --git a/doc/design/roadmap.mdwn b/doc/design/roadmap.mdwn
--- a/doc/design/roadmap.mdwn
+++ b/doc/design/roadmap.mdwn
@@ -9,10 +9,10 @@
 * Month 3 user-driven features and polishing [[todo/direct_mode_guard]] [[assistant/upgrading]]
 * Month 4 [[Windows_webapp|assistant/Windows]], Linux arm, [[todo/support_for_writing_external_special_remotes]]
 * Month 5 user-driven features and polishing
-* **Month 6 get [[assistant/Android]] and Windows out of beta**
+* **Month 6 get Windows out of beta, [[metadata and views|design/metadata]]**
 * Month 7 user-driven features and polishing
 * Month 8 [[!traillink assistant/telehash]]
 * Month 9 [[!traillink assistant/gpgkeys]] [[!traillink assistant/sshpassword]]
-* Month 10 user-driven features and polishing
+* Month 10 get [[assistant/Android]] out of beta
 * Month 11 [[!traillink assistant/chunks]] [[!traillink assistant/deltas]]
 * Month 12 user-driven features and polishing
diff --git a/doc/devblog/day_111__windows_beta_release.mdwn b/doc/devblog/day_111__windows_beta_release.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_111__windows_beta_release.mdwn
@@ -0,0 +1,6 @@
+Pushed out the new release. This is the first one where I consider the
+git-annex command line beta quality on Windows.
+
+Did some testing of the webapp on Windows, trying out every part of the UI.
+I now have eleven todo items involving the webapp listed in
+[[todo/windows_support]]. Most of them don't look too bad to fix.
diff --git a/doc/devblog/day_112__metadata_design.mdwn b/doc/devblog/day_112__metadata_design.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_112__metadata_design.mdwn
@@ -0,0 +1,18 @@
+There's a new design document for letting git-annex store arbitrary
+metadata. The really neat thing about this is the user can check out only
+files matching the tags or values they care about, and get an automatically
+structuted file tree layout that can be dynamically filtered. It's going to
+be awesome! [[design/metadata]]
+
+In the meantime, spent most of today working on Windows. Very good
+progress, possibly motivated by wanting to get it over with so I can spend
+some time this month on the above. ;)
+
+* webapp can make box.com and S3 remotes. This just involved fixing a hack
+  where the webapp set environment variables to communicate creds to
+  initremote. Can't change environment on Windows (or I don't know how to).
+* webapp can make repos on removable drives.
+* `git annex assistant --stop` works, although this is not likely to really
+  be useful
+* The source tree now has 0 `func = error "Windows TODO"` type stubbed out
+  functions to trip over.
diff --git a/doc/devblog/day_113__metadata_groundwork.mdwn b/doc/devblog/day_113__metadata_groundwork.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_113__metadata_groundwork.mdwn
@@ -0,0 +1,9 @@
+Built the core data types, and log for metadata storage. Making metadata
+union merge well is tricky, but I have a design I'm happy with, that will
+allow distributed changes to metadata.
+
+Finished up the day with a `git annex metadata` command to get/set metadata
+for a file.
+
+This is all the goundwork needed to begin experimenting with generating
+git branches that display different metadata-driven views of annexed files.
diff --git a/doc/devblog/day_114__windows_porting.mdwn b/doc/devblog/day_114__windows_porting.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_114__windows_porting.mdwn
@@ -0,0 +1,8 @@
+Windows porting all day. Fixed a lot of issues with the webapp,
+so quite productive. Except for the 2 hours wasted finding a way to kill a
+process by PID from Haskell on Windows.
+
+Last night, made `git annex metadata` able to set metadata on a whole
+directory or list of files if desired. And added a `--metadata field=value`
+switch (and corresponding preferred content terminal) which limits
+git-annex to acting on files with the specified metadata.
diff --git a/doc/devblog/day_115__windows_porting.mdwn b/doc/devblog/day_115__windows_porting.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_115__windows_porting.mdwn
@@ -0,0 +1,17 @@
+More Windows porting.. Seem to be getting near an end of the easy stuff,
+and also the webapp is getting pretty usable on Windows now, the only
+really important thing lacking is XMPP support.
+
+Made git-annex on Windows set HOME when it's not already set. Several of
+the bundled cygwin tools only look at HOME. This was made a lot harder and
+uglier due to there not being any way to modify the environment of the
+running process.. git-annex has to re-run itself with the fixed
+environment.
+
+Got rsync.net working in the webapp. Although with an extra rsync.net
+password prompt on Windows, which I cannot find a way to avoid.
+
+While testing that, I discovered that openssh 6.5p1 has broken support for
+~/.ssh/config Host lines that contain upper case letters! I have filed a
+bug about this and put a quick fix in git-annex, which sometimes generated
+such lines.
diff --git a/doc/devblog/day_116__views.mdwn b/doc/devblog/day_116__views.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_116__views.mdwn
@@ -0,0 +1,54 @@
+Working on building [[design/metadata]] filtered branches.
+
+Spent most of the day on types and pure code. Finally at the end 
+I wrote down two actions that I still need to implement to make
+it all work:
+
+[[!format haskell """
+applyView' :: MkFileView -> View -> Annex Git.Branch
+updateView :: View -> Git.Ref -> Git.Ref -> Annex Git.Branch
+"""]]
+
+I know how to implement these, more or less. And in most cases
+they will be pretty fast.
+
+The more interesting part is already done. That was the issue of how to
+generate filenames in the filter branches. That depends on the `View` being
+used to filter and organize the branch, but also on the original filename used
+in the reference branch. Each filter branch has a reference branch (such as
+"master"), and displays a filtered and metadata-driven reorganized tree
+of files from its reference branch.
+
+[[!format haskell """
+fileViews :: View -> (FilePath -> FileView) -> FilePath -> MetaData -> Maybe [FileView]
+"""]]
+
+So, a view that matches files tagged "haskell" or "git-annex"
+and with an author of "J\*" will generate filenames like 
+"haskell/Joachim/interesting_theoretical_talk.ogg" and
+"git-annex/Joey/mytalk.ogg".
+
+It can also work backwards from these 
+filenames to derive the MetaData that is encoded in them.
+
+[[!format haskell """
+fromView :: View -> FileView -> MetaData
+"""]]
+
+So, copying a file to "haskell/Joey/mytalk.ogg" lets it know that
+it's gained a "haskell" tag. I knew I was on the right track when
+`fromView` turned out to be only 6 lines of code!
+
+The trickiest part of all this, which I spent most of yesterday thinking
+about, is what to do if the master branch has files in subdirectories. It
+probably does not makes sense to retain that hierarchical directory
+structure in the filtered branch, because we instead have a
+non-hierarchical metadata structure to express. (And there would probably
+be a lot of deep directory structures containing only one file.) But
+throwing away the subdirectory information entirely means that two files
+with the same basename and same metadata would have colliding names.
+
+I eventually decided to embed the subdirectory information into the filenames
+used on the filter branch. Currently that is done by converting
+`dir/subdir/file.foo` to `file(dir)(subdir).foo`. We'll see how this works
+out in practice..
diff --git a/doc/devblog/day_117__views_implemented.mdwn b/doc/devblog/day_117__views_implemented.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_117__views_implemented.mdwn
@@ -0,0 +1,76 @@
+Today I built `git annex view`, and `git annex vadd` and a few related
+commands. A quick demo:
+
+<pre>
+joey@darkstar:~/lib/talks>ls
+Chaos_Communication_Congress/  FOSDEM/       Linux_Conference_Australia/
+Debian/                        LibrePlanet/  README.md
+joey@darkstar:~/lib/talks>git annex view tag=*
+view  (searching...)
+Switched to branch 'views/_'
+ok
+joey@darkstar:~/lib/talks#_>tree -d
+.
+|-- Debian
+|-- android
+|-- bigpicture
+|-- debhelper
+|-- git
+|-- git-annex
+`-- seen
+
+7 directories
+joey@darkstar:~/lib/talks#_>git annex vadd author=*
+vadd  
+Switched to branch 'views/author=_;_'
+ok
+joey@darkstar:~/lib/talks#author=_;_>tree -d
+.
+|-- Benjamin Mako Hill
+|   `-- bigpicture
+|-- Denis Carikli
+|   `-- android
+|-- Joey Hess
+|   |-- Debian
+|   |-- bigpicture
+|   |-- debhelper
+|   |-- git
+|   `-- git-annex
+|-- Richard Hartmann
+|   |-- git
+|   `-- git-annex
+`-- Stefano Zacchiroli
+    `-- Debian
+
+15 directories
+joey@darkstar:~/lib/talks#author=_;_>git annex vpop
+vpop 1
+Switched to branch 'views/_'
+ok
+joey@darkstar:~/lib/talks#_>git annex vadd tag=git-annex
+vadd  
+Switched to branch 'views/(git-annex)'
+ok
+joey@darkstar:~/lib/talks#(git-annex)>ls
+1025_gitify_your_life_{Debian;2013;DebConf13;high}.ogv@
+git_annex___manage_files_with_git__without_checking_their_contents_into_git_{FOSDEM;2012;lightningtalks}.webm@
+mirror.linux.org.au_linux.conf.au_2013_mp4_gitannex_{Linux_Conference_Australia;2013}.mp4@
+joey@darkstar:~/lib/talks#_>git annex vpop 2
+vpop 2
+Switched to branch 'master'
+ok
+</pre>
+
+Not 100% happy with the speed -- the generation of the view branch is close
+to optimal, and fast enough (unless the branch has very many matching
+files). And `vadd` can be quite fast if the view has already limited the
+total number of files to a smallish amount. But `view` has to look at every
+file's metadata, and this can take a while in a large repository. Needs indexes.
+
+It also needs integration with `git annex sync`, so the view branches
+update when files are added to the master branch, and moving files around
+inside a view and committing them does not yet update their metadata.
+
+---
+
+Today's work was sponsored by Daniel Atlas.
diff --git a/doc/devblog/day_118__views_refined.mdwn b/doc/devblog/day_118__views_refined.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_118__views_refined.mdwn
@@ -0,0 +1,7 @@
+Still working on views. The most important addition today is that
+`git annex precommit` notices when files have been moved/copied/deleted
+in a view, and updates the metadata to reflect the changes.
+
+Also wrote some walkthrough documentation: [[tips/metadata_driven_views]].  
+And, recorded a screencast demoing views, which I will upload next time
+I have bandwidth.
diff --git a/doc/devblog/day_119__catching_up.mdwn b/doc/devblog/day_119__catching_up.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/devblog/day_119__catching_up.mdwn
@@ -0,0 +1,15 @@
+Spent the day catching up on the last week or so's traffic. Ended up
+making numerous small big fixes and improvements. Message backlog stands at
+44.
+
+Here's the [[screencast demoing views|videos/git-annex_views_demo]]!
+
+Added to the design today the idea of
+automatically deriving metadata from the location of files in the master
+branch's directory tree. Eg, `git annex view tag=* podcasts/=*` in a
+repository that has a `podcasts/` directory would make a tree like
+"$tag/$podcast". Seems promising. 
+
+So much still to do with views.. I have belatedly added them to
+the roadmap for this month; doing Windows and Android in the same month was
+too much to expect.
diff --git a/doc/forum/alternativeto.net___34__Like__34__.mdwn b/doc/forum/alternativeto.net___34__Like__34__.mdwn
--- a/doc/forum/alternativeto.net___34__Like__34__.mdwn
+++ b/doc/forum/alternativeto.net___34__Like__34__.mdwn
@@ -1,3 +1,3 @@
 When I went to alternativeto.net I noticed that SpiderOak is a featured application. I decided to search git-annex and see how "Like"-ed it is in comparison... there were 0 "Like"-s.
 
-I suggest going to http://alternativeto.net/software/git-annex/ and "Like" git-annex.
+I suggest going to <http://alternativeto.net/software/git-annex/> and "Like" git-annex.
diff --git a/doc/forum/not_finding_git-annex-shell_on_remote.mdwn b/doc/forum/not_finding_git-annex-shell_on_remote.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/forum/not_finding_git-annex-shell_on_remote.mdwn
@@ -0,0 +1,21 @@
+I have set up an annex on a remote machine and I am connecting via ssh.  But, since it is a managed machine, I installed the git-annex binary in my own ~/bin.  Well, when I try 
+$git annex sync
+
+I get:
+  $git annex sync
+(merging origin/git-annex into git-annex...)
+(Recording state in git...)
+bash: git-annex-shell: command not found
+
+  Remote origin does not have git-annex installed; setting annex-ignore
+commit  ok
+pull origin 
+
+merge: refs/remotes/origin/master - not something we can merge
+
+merge: refs/remotes/origin/synced/master - not something we can merge
+failed
+git-annex: sync: 1 failed
+
+
+The git remote -v looks correct.  So, how do I tell git annex on my local machine where to use $HOME/bin in PATH on the remote machine when syncing  with remotes?
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -503,9 +503,10 @@
   To avoid expensive checksum calculations (and expensive transfers when
   fscking a remote), specify `--fast`.
 
-  To start a new incremental fsck, specify `--incremental`. Then
-  the next time you fsck, you can specify `--more` to skip over
-  files that have already been checked, and continue where it left off.
+  To start a new incremental fsck, use the `--incremental` option. Then
+  the next time you fsck, you can instead use the `--more` option 
+  to skip over files that have already been checked, and continue
+  where it left off.
 
   The `--incremental-schedule` option makes a new incremental fsck be
   started a configurable time after the last incremental fsck was started.
@@ -693,6 +694,72 @@
   Note that this subcommand can be used to graph any git repository; it
   is not limited to git-annex repositories.
 
+# METADATA COMMANDS
+
+* `metadata [path ...] [-s field=value -s field+=value -s field-=value ...]`
+
+  Each file can have any number of metadata fields attached to it,
+  which each in turn have any number of values. This sets metadata
+  for the specified file or files, or if run without any values, shows
+  the current metadata.
+
+  To set a field's value, removing any old value(s), use -s field=value.
+
+  To add an additional value, use -s field+=value.
+
+  To remove a value, use -s field-=value.
+
+  To set a tag, use -t tag, and use -u tag to remove a tag.
+
+  For example, to set some tags on a file and also its author:
+
+        	git annex metadata annexscreencast.ogv -t video -t screencast -s author+=Alice
+
+* `view [field=value ...] [tag ...]`
+
+  Uses metadata to build a view branch of the files in the current branch,
+  and checks out the view branch. Only files in the current branch whose
+  metadata matches all the specified field values and tags will be
+  shown in the view.
+  
+  Multiple values for a metadata field can be specified, either by using
+  a glob (`field="*"`) or by listing each wanted value. The resulting view
+  will put files in subdirectories according to the value of their fields.
+
+  Once within a view, you can make additional directories, and
+  copy or move files into them. When you commit, the metadata will
+  be updated to correspond to your changes.
+
+* `vpop [N]`
+
+  Switches from the currently active view back to the previous view.
+  Or, from the first view back to original branch.
+
+  The optional number tells how many views to pop.
+
+* `vfilter [field=value ...] [tag ...]`
+
+  Filters the current view to only the files that have the
+  specified values and tags.
+
+* `vadd [field=glob ...]`
+
+  Changes the current view, adding an additional level of directories
+  to categorize the files.
+
+  For example, when the view is by author/tag, `vadd year=*` will 
+  change it to year/author/tag. 
+  
+  So will `vadd year=2014 year=2013`, but limiting the years in view
+  to only those two.
+
+* `vcycle`
+
+  When a view involves nested subdirectories, this cycles the order.
+
+  For example, when the view is by year/author/tag, `vcycle` will switch
+  it to author/tag/year.
+
 # UTILITY COMMANDS
 
 * `migrate [path ...]`
@@ -753,7 +820,8 @@
 
   Fixes up symlinks that are staged as part of a commit, to ensure they
   point to annexed content. Also handles injecting changes to unlocked
-  files into the annex.
+  files into the annex. When in a view, updates metadata to reflect changes
+  made to files in the view.
 
 * `lookupkey [file ...]`
 
@@ -1065,6 +1133,11 @@
   The size can be specified with any commonly used units, for example,
   "0.5 gb" or "100 KiloBytes"
 
+* `--metadata field=value`
+
+  Matches only files that have a metadata field attached with the specified
+  value.
+
 * `--want-get`
 
   Matches files that the preferred content settings for the repository
@@ -1552,6 +1625,15 @@
 	*.flac annex.numcopies=3
 
 Note that setting numcopies to 0 is very unsafe.
+
+These settings are honored by git-annex whenever it's operating on a
+matching file. However, when using --all, --unused, or --key to specify
+keys to operate on, git-annex is operating on keys and not files, so will
+not honor the settings from .gitattributes.
+
+Also note that when using views, only the toplevel .gitattributes file is
+preserved in the view, so other settings in other files won't have any
+efffect.
 
 # FILES
 
diff --git a/doc/install/OSX/comment_34_874ff01f27911baf6ef0f559d5d5f5a0._comment b/doc/install/OSX/comment_34_874ff01f27911baf6ef0f559d5d5f5a0._comment
new file mode 100644
--- /dev/null
+++ b/doc/install/OSX/comment_34_874ff01f27911baf6ef0f559d5d5f5a0._comment
@@ -0,0 +1,27 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawn3rK4VDzxyhmrIc18z7F5OuXvEbUsgUac"
+ nickname="Srinath"
+ subject="build issue with brew technique on Darwin Kernel Version 13.0.0"
+ date="2014-02-15T02:17:16Z"
+ content="""
+Following the Mac OS X brew instructions from the top of the board, I got the following error:
+
+[5 of 5] Compiling Yesod            ( Yesod.hs, dist/build/Yesod.o )
+In-place registering yesod-1.2.5...
+Installing library in /Users/srinathv/.cabal/lib/yesod-1.2.5/ghc-7.6.3
+Registering yesod-1.2.5...
+Installed yesod-1.2.5
+cabal: Error: some packages failed to install:
+git-annex-5.20140210 depends on libxml-sax-0.7.4 which failed to install.
+libxml-sax-0.7.4 failed during the configure step. The exception was:
+ExitFailure 1
+network-protocol-xmpp-0.4.5 depends on libxml-sax-0.7.4 which failed to
+install.
+
+
+Then I perused the comments and did:
+$brew link libmxl2 --force
+$cabal install git-annex --bindir=$HOME/bin
+
+with success.  
+"""]]
diff --git a/doc/internals.mdwn b/doc/internals.mdwn
--- a/doc/internals.mdwn
+++ b/doc/internals.mdwn
@@ -146,6 +146,27 @@
 	1287290776.765152s e605dca6-446a-11e0-8b2a-002170d25c55 blah blah
 	1287290767.478634s 26339d22-446b-11e0-9101-002170d25c55 foo=bar
 
+## `aaa/bbb/*.log.met`
+
+These log files are used to store arbitrary [[design/metadata]] about keys.
+Each key can have any number of metadata fields. Each field has a set of
+values.
+
+Lines are timestamped, and record when values are added (`field +value`),
+but also when values are removed (`field -value`). Removed values
+are retained in the log so that when merging an old line that sets a value
+that was later unset, the value is not accidentially added back.
+
+For example:
+
+	1287290776.765152s tag +foo +bar author +joey
+	1291237510.141453s tag -bar +baz
+
+The value can be completely arbitrary data, although it's typically
+reasonably short. If the value contains any whitespace
+(including \r or \r), it will be base64 encoded. Base64 encoded values
+are indicated by prefixing them with "!" 
+
 ## `schedule.log`
 
 Used to record scheduled events, such as periodic fscks.
diff --git a/doc/news/version_5.20140116.mdwn b/doc/news/version_5.20140116.mdwn
deleted file mode 100644
--- a/doc/news/version_5.20140116.mdwn
+++ /dev/null
@@ -1,21 +0,0 @@
-git-annex 5.20140116 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
-   * Added tahoe special remote.
-   * external special remote protocol: Added GETGITDIR, and GETAVAILABILITY.
-   * Refuse to build with git older than 1.7.1.1, which is needed for
-     git checkout -B
-   * map: Fix display of v5 direct mode repos.
-   * repair: Support old git versions from before git fsck --no-dangling was
-     implemented.
-   * Fix a long-standing bug that could cause the wrong index file to be used
-     when committing to the git-annex branch, if GIT\_INDEX\_FILE is set in the
-     environment. This typically resulted in git-annex branch log files being
-     committed to the master branch and later showing up in the work tree.
-     (These log files can be safely removed.)
-   * assistant: Detect if .git/annex/index is corrupt at startup, and
-     recover.
-   * repair: Fix bug in packed refs file exploding code that caused a .gitrefs
-     directory to be created instead of .git/refs
-   * Fix FTBFS on mipsel and sparc due to test suite not being available
-     on those architectures.
-   * Android: Avoid passing --clobber to busybox wget."""]]
diff --git a/doc/news/version_5.20140221.mdwn b/doc/news/version_5.20140221.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/news/version_5.20140221.mdwn
@@ -0,0 +1,28 @@
+git-annex 5.20140221 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+   * metadata: New command that can attach metadata to files.
+   * --metadata can be used to limit commands to acting on files
+     that have particular metadata.
+   * Preferred content expressions can use metadata=field=value
+     to limit them to acting on files that have particular metadata.
+   * view: New command that creates and checks out a branch that provides
+     a structured view of selected metadata.
+   * vfilter, vadd, vpop, vcycle: New commands for operating within views.
+   * pre-commit: Update metadata when committing changes to locations
+     of annexed files within a view.
+   * Add progress display for transfers to/from external special remotes.
+   * unused: Fix to actually detect unused keys when in direct mode.
+   * fsck: When run with --all or --unused, while .gitattributes
+     annex.numcopies cannot be honored since it's operating on keys
+     instead of files, make it honor the global numcopies setting,
+     and the annex.numcopies git config setting.
+   * trust, untrust, semitrust, dead: Warn when the trust level is
+     overridden in .git/config.
+   * glacier: Do not try to run glacier value create when an existing glacier
+     remote is enabled.
+   * fsck: Refuse to do anything if more than one of --incremental, --more,
+     and --incremental-schedule are given, since it's not clear which option
+     should win.
+   * Windows webapp: Can set up box.com, Amazon S3, and rsync.net remotes
+   * Windows webapp: Can create repos on removable drives.
+   * Windows: Ensure HOME is set, as needed by bundled cygwin utilities."""]]
diff --git a/doc/special_remotes.mdwn b/doc/special_remotes.mdwn
--- a/doc/special_remotes.mdwn
+++ b/doc/special_remotes.mdwn
@@ -38,6 +38,7 @@
 * [[IMAP|forum/special_remote_for_IMAP]]
 * [[Usenet|forum/nntp__47__usenet special remote]]
 * [chef-vault](https://github.com/3ofcoins/knife-annex/)
+* [hubiC](https://github.com/Schnouki/git-annex-remote-hubic)
 
 Want to add support for something else? [[Write your own!|external]]
 
diff --git a/doc/tips/metadata_driven_views.mdwn b/doc/tips/metadata_driven_views.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/tips/metadata_driven_views.mdwn
@@ -0,0 +1,121 @@
+git-annex now has support for storing 
+[[arbitrary metadata|design/metadata]] about annexed files. For example, this can be
+used to tag files, to record the author of a file, etc. The metadata is
+synced around between repositories with the other information git-annex
+keeps track of.
+
+One nice way to use the metadata is through **views**. You can ask
+git-annex to create a view of files in the currently checked out branch
+that have certian metadata. Once you're in a view, you can move and copy
+files to adjust their metadata further. Rather than the traditional
+hierarchical directory structure, views are dynamic; you can easily
+refine or reorder a view.
+
+Let's get started by setting some tags on files. No views yet, just some
+metadata:
+
+	# git annex metadata --tag todo work/2014/*
+	# git annex metadata --untag todo work/2014/done/*
+	# git annex metadata --tag urgent work/2014/presentation_for_tomorrow.odt
+	# git annex metadata --tag done work/2013/* work/2014/done/*
+	# git annex metadata --tag work work
+	# git annex metadata --tag video videos
+	# git annex metadata --tag work videos/operating_heavy_machinery.mov
+	# git annex metadata --tag done videos/old
+	# git annex metadata --tag new videos/lotsofcats.ogv
+	# git annex metadata --tag sound podcasts
+	# git annex metadata --tag done podcasts/old
+	# git annex metadata --tag new podcasts/recent
+
+So, you had a bunch of different kinds of files sorted into a directory
+structure. But that didn't really reflect how you approach the files.
+Adding some tags lets you categorize the files in different ways.
+
+Ok, metadata is in place, but how to use it? Time to change views!
+
+	# git annex view tag=*
+	view  (searching...)
+
+	Switched to branch 'views/_'
+	ok
+
+This searched for all files with any tag, and created a new git branch
+that sorts the files according to their tags.
+
+	# tree -d
+	work
+	todo
+	urgent
+	done
+	new
+	video
+	sound
+
+Notice that a single file may appear in multiple directories
+depending on its tags. For example, `lotsofcats.ogv` is in
+both `new/` and `video/`.
+
+Ah, but you're at work now, and don't want to be distracted by cat videos.
+Time to filter the view:
+
+	# git annex vfilter tag=work
+	vfilter
+	Switched to branch 'views/(work)/_'
+	ok
+
+Now only the work files are in the view, and they're otherwise categorized
+according to their other tags. So you can check the `urgent/` directory
+to see what's next, and look in `todo/` for other work related files.
+
+Now that you're in a tag based view, you can move files around between the
+directories, and when you commit your changes to git, their tags will be
+updated.
+
+	# git mv urgent/presentation_for_tomorrow_{work;2014}.odt ../done
+	# git commit -m "a good day's work"
+	metadata tag-=urgent
+	metadata tag+=done
+
+You can return to a previous view by running `git annex vpop`. If you pop
+all the way out of all views, you'll be back on the regular git branch you
+originally started from. You can also use `git checkout` to switch between
+views and other branches.
+
+Beyond simple tags, you can add whatever kinds of metadata you like, and
+use that metadata in more elaborate views. For example, let's add a year
+field.
+
+	# git checkout master
+	# git annex metadata --set year=2014 work/2014
+	# git annex metadata --set year=2013 work/2013
+	# git annex view year=* tag=*
+
+Now you're in a view with two levels of directories, first by year and then
+by tag.
+
+	# tree -d
+	2014
+	  |-- work
+          |-- todo
+	  |-- urgent
+          `-- done
+	2013
+	  |-- work
+	  `-- done
+
+Oh, did you want it the other way around? Easy!
+
+	# git annex vcycle
+	# tree -d
+	work
+	  |-- 2014
+	  `-- 2013
+	todo
+	  `-- 2014
+	urgent
+	  `-- 2014
+	done
+	  |-- 2014
+	  `-- 2013
+
+This has probably only scratched the surface of what you can do with views.
diff --git a/doc/tips/using_Amazon_Glacier.mdwn b/doc/tips/using_Amazon_Glacier.mdwn
--- a/doc/tips/using_Amazon_Glacier.mdwn
+++ b/doc/tips/using_Amazon_Glacier.mdwn
@@ -24,7 +24,7 @@
 
         # cd /media/usb/annex
         # git pull laptop
-        # git annex initremote glacier
+        # git annex enableremote glacier
         initremote glacier (gpg) ok
 
 Now the remote can be used like any other remote.
diff --git a/doc/todo/Use_bitcoin-mining_ASICs_for_hashing__63__.mdwn b/doc/todo/Use_bitcoin-mining_ASICs_for_hashing__63__.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/Use_bitcoin-mining_ASICs_for_hashing__63__.mdwn
@@ -0,0 +1,18 @@
+This is just an idea, and I have no idea if it would work (that's why I'm asking):
+
+**Would it be possible to use ASICs made for Bitcoin mining inside git-annex to offload the hashing of files?**
+
+I got the idea, because I have two RaspberryPis here: 
+
+- one runs my git-annex archive. It is really slow at hashing, so I resorted to using the WORM backend
+- another one runs 2 old-ish ASIC miners. They are just barely "profitable" right now, so in a few months they will be obsolete
+
+Both devices to some kind of `SHA256`. I have a feeling this is either extremely easy or extremely complicated to do… :)
+
+> git-annex uses binaries such as `sha256sum` for hashing large files (large is
+> currently hardcoded as bigger than 1MB). If you insert a binary with the same
+> interface as `sha256sum` into your `$PATH`, git-annex will automatically use
+> it. If you want to use ASIC hashing even for small files, you need to tweak
+> `Backend/Hash.hs`. --[[HelmutGrohne]]
+
+>> [[done]] --[[Joey]]
diff --git a/doc/todo/add_metadata_to_annexed_files.mdwn b/doc/todo/add_metadata_to_annexed_files.mdwn
--- a/doc/todo/add_metadata_to_annexed_files.mdwn
+++ b/doc/todo/add_metadata_to_annexed_files.mdwn
@@ -1,5 +1,14 @@
-I would like to attach metadata to annexed files (objects) without cluttering the workdir with files containing this metadata. A common use case would be to add titles to my photo collection that could than end up in a generated photo album.
+I would like to attach metadata to annexed files (objects) without
+cluttering the workdir with files containing this metadata. A common use
+case would be to add titles to my photo collection that could than end up
+in a generated photo album.
 
 Depending on the implementation it might also be possible to use the metadata facility for a threaded commenting system.
 
-The first question is whether the metadata is attached to the objects and thus shared by all paths pointing to the same data object or to paths in the worktree. I've no preference here at this point.
+The first question is whether the metadata is attached to the objects and
+thus shared by all paths pointing to the same data object or to paths in
+the worktree. I've no preference here at this point.
+
+> This is [[done]]; see [[design/metadata]].
+> The metadata is attached to objects, not to files.
+> --[[Joey]] 
diff --git a/doc/todo/openwrt_package/comment_2_2cb7dd4c0cc4413a4588b13cf7700de2._comment b/doc/todo/openwrt_package/comment_2_2cb7dd4c0cc4413a4588b13cf7700de2._comment
new file mode 100644
--- /dev/null
+++ b/doc/todo/openwrt_package/comment_2_2cb7dd4c0cc4413a4588b13cf7700de2._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawkNE-H4vEcbcGndxq5daT8qUb7yIf7r1OE"
+ nickname="Łukasz"
+ subject="comment 2"
+ date="2014-02-11T21:05:00Z"
+ content="""
+if debian does not have working toolchain for mips then there is no way to port it into mips machines.
+am i correct ?
+"""]]
diff --git a/doc/todo/openwrt_package/comment_3_5ba8a325a683ff543d81a366c873070d._comment b/doc/todo/openwrt_package/comment_3_5ba8a325a683ff543d81a366c873070d._comment
new file mode 100644
--- /dev/null
+++ b/doc/todo/openwrt_package/comment_3_5ba8a325a683ff543d81a366c873070d._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ ip="209.250.56.172"
+ subject="comment 3"
+ date="2014-02-11T21:39:34Z"
+ content="""
+Actually, debian stable does still have ghc building for mips/mipsel. It's just newer versions that have failed to build and nobody has fixed it yet.
+"""]]
diff --git a/doc/todo/whislist:_allow_setting_annex-ignore_from_the_webapp.mdwn b/doc/todo/whislist:_allow_setting_annex-ignore_from_the_webapp.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/whislist:_allow_setting_annex-ignore_from_the_webapp.mdwn
@@ -0,0 +1,2 @@
+I would like to be able to set 'annex-ignore' for remote servers through the webapp.
+Maybe a checkbox beneath "Syncing enabled" with something like "Also sync content" that it's checked by default?
diff --git a/doc/todo/windows_support.mdwn b/doc/todo/windows_support.mdwn
--- a/doc/todo/windows_support.mdwn
+++ b/doc/todo/windows_support.mdwn
@@ -3,22 +3,11 @@
 
 ## status
 
-* Does not work with Cygwin's build of git (that git does not consistently
-  support use of DOS style paths, which git-annex uses on Windows). 
-  Must use Msysgit.
-* rsync special remotes with a rsyncurl of a local directory are known
-			let r = if inr1 then r1 else r2
-  buggy. (git-annex tells rsync C:foo and it thinks it means a remote host
-  named C...)
-* Ssh connection caching does not work on Windows, so `git annex get`
-  has to connect twice to the remote system over ssh per file, which
-  is much slower than on systems supporting connection caching.
-* `git annex assistant` has not been tested, is probably quite incomplete
-  and/or buggy.
 * Doesn't daemonize. Maybe use
   <http://hackage.haskell.org/package/Win32-services>
   or perhaps easier,
   <http://hackage.haskell.org/package/Win32-services-wrapper>
+
 * XMPP library not yet built.
   
   This should work to install the deps, using libs from cygwin
@@ -34,3 +23,42 @@
 
   Also needs gsasl, which is not in cygwin. 
   See <http://josefsson.org/gsasl4win/README.html>
+
+* View debug log is empty in windows -- all logs go to console.
+  This messes up a few parts of UI that direct user to the debug log.
+  Should try to get rid of the console, but only once ssh passwords
+  (and possibly gpg) are not prompted there anymore.
+
+* Local pairing seems to fail, after acking on Linux box, it stalls.
+
+* gcrypt is not ported to windows (and as a shell script, may need
+  to be rewritten)
+
+* Incremental fsck sets the sticky bit to record when a file is fscked,
+  and this is not done on windows, so fsck doesn't behave incrementally
+  there.
+
+* Deleting a git repository from inside the webapp fails "RemoveDirectory
+  permision denied ... file is being used by another process"
+
+## minor problems
+
+* Does not work with Cygwin's build of git (that git does not consistently
+  support use of DOS style paths, which git-annex uses on Windows). 
+  Must use Msysgit.
+* rsync special remotes with a rsyncurl of a local directory are known
+  buggy. (git-annex tells rsync C:foo and it thinks it means a remote host
+  named C...)
+* webapp lets user choose to encrypt repo, and generate gpg key,
+  before checking that gcrypt is not installed
+* Ssh connection caching does not work on Windows, so `git annex get`
+  has to connect twice to the remote system over ssh per file, which
+  is much slower than on systems supporting connection caching.
+* glacier-cli is not easily available (probably)
+
+## stuff needing testing
+
+* test S3 and box.com setup in webapp now that they should work..
+* test that adding a repo on a removable drive works; that git is synced to
+  it and files can be transferred to it and back
+* Does stopping in progress transfers work in the webapp?
diff --git a/doc/todo/wishlist:_more_descriptive_commit_messages_in_git-annex_branch.mdwn b/doc/todo/wishlist:_more_descriptive_commit_messages_in_git-annex_branch.mdwn
--- a/doc/todo/wishlist:_more_descriptive_commit_messages_in_git-annex_branch.mdwn
+++ b/doc/todo/wishlist:_more_descriptive_commit_messages_in_git-annex_branch.mdwn
@@ -37,3 +37,19 @@
 
 >> Closing as this is literally impossible to do without making
 >> git-annex worse. [[done]] --[[Joey]] 
+
+> I'm not sure that the requested feature is that far off. There are two
+> aspects, that can be solved relatively easy:
+>
+>  * Recording the name of the remote the commit was issued on. This
+>    information is simply constant per remote.
+>
+>  * While it is true that there is no 1 on 1 correspondence between commands
+>    and git-annex commits, it would be entirely possible to add a "message
+>    journal". Every command issued would start out with writing its
+>    invocation to the message journal. At the time the journal ends up being
+>    committed to the git-annex branch, the message journal is used as the
+>    body of the commit message and truncated.
+>
+> It is true that these suggestions do not address every aspect of the
+> original report, but they would solve about 90%. --[[HelmutGrohne]]
diff --git a/doc/videos/git-annex_views_demo.mdwn b/doc/videos/git-annex_views_demo.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/git-annex_views_demo.mdwn
@@ -0,0 +1,11 @@
+A quick screencast demoing an experimental new feature,
+[[tips/metadata_driven_views]].
+
+<video controls src="https://downloads.kitenet.net/videos/git-annex/git-annex_views_demo.ogg"></video>
+
+[video](https://downloads.kitenet.net/videos/git-annex/git-annex_views_demo.ogg)
+
+Credits: 
+
+* RichiH for <https://github.com/RichiH/conference_proceedings>
+* Michi for keyboard cat cameo
diff --git a/doc/walkthrough/backups.mdwn b/doc/walkthrough/backups.mdwn
--- a/doc/walkthrough/backups.mdwn
+++ b/doc/walkthrough/backups.mdwn
@@ -1,15 +1,17 @@
 git-annex can be configured to require more than one copy of a file exists,
-as a simple backup for your data. This is controlled by the "annex.numcopies"
-setting, which defaults to 1 copy. Let's change that to require 2 copies,
-and send a copy of every file to a USB drive.
+as a simple backup for your data. This is controlled by the
+numcopies setting, which defaults to 1 copy. Let's
+change that to require 2 copies, and send a copy of every file
+to a USB drive.
 
-	# echo "* annex.numcopies=2" >> .gitattributes
+	# git annex numcopies 2
 	# git annex copy . --to usbdrive
 
 Now when we try to `git annex drop` a file, it will verify that it
 knows of 2 other repositories that have a copy before removing its
 content from the current repository.
 
+The numcopies setting used above is the global default.
 You can also vary the number of copies needed, depending on the file name.
 So, if you want 3 copies of all your flac files, but only 1 copy of oggs:
 
diff --git a/git-annex.1 b/git-annex.1
--- a/git-annex.1
+++ b/git-annex.1
@@ -463,9 +463,10 @@
 To avoid expensive checksum calculations (and expensive transfers when
 fscking a remote), specify \fB\-\-fast\fP.
 .IP
-To start a new incremental fsck, specify \fB\-\-incremental\fP. Then
-the next time you fsck, you can specify \fB\-\-more\fP to skip over
-files that have already been checked, and continue where it left off.
+To start a new incremental fsck, use the \fB\-\-incremental\fP option. Then
+the next time you fsck, you can instead use the \fB\-\-more\fP option 
+to skip over files that have already been checked, and continue
+where it left off.
 .IP
 The \fB\-\-incremental\-schedule\fP option makes a new incremental fsck be
 started a configurable time after the last incremental fsck was started.
@@ -639,6 +640,66 @@
 Note that this subcommand can be used to graph any git repository; it
 is not limited to git\-annex repositories.
 .IP
+.SH METADATA COMMANDS
+.IP "\fBmetadata [path ...] [\-s field=value \-s field+=value \-s field\-=value ...]\fP"
+.IP
+Each file can have any number of metadata fields attached to it,
+which each in turn have any number of values. This sets metadata
+for the specified file or files, or if run without any values, shows
+the current metadata.
+.IP
+To set a field's value, removing any old value(s), use \-s field=value.
+.IP
+To add an additional value, use \-s field+=value.
+.IP
+To remove a value, use \-s field\-=value.
+.IP
+To set a tag, use \-t tag, and use \-u tag to remove a tag.
+.IP
+For example, to set some tags on a file and also its author:
+.IP
+ git annex metadata annexscreencast.ogv \-t video \-t screencast \-s author+=Alice
+.IP
+.IP "\fBview [field=value ...] [tag ...]\fP"
+Uses metadata to build a view branch of the files in the current branch,
+and checks out the view branch. Only files in the current branch whose
+metadata matches all the specified field values and tags will be
+shown in the view.
+.IP
+Multiple values for a metadata field can be specified, either by using
+a glob (\fBfield="*"\fP) or by listing each wanted value. The resulting view
+will put files in subdirectories according to the value of their fields.
+.IP
+Once within a view, you can make additional directories, and
+copy or move files into them. When you commit, the metadata will
+be updated to correspond to your changes.
+.IP
+.IP "\fBvpop [N]\fP"
+Switches from the currently active view back to the previous view.
+Or, from the first view back to original branch.
+.IP
+The optional number tells how many views to pop.
+.IP
+.IP "\fBvfilter [field=value ...] [tag ...]\fP"
+Filters the current view to only the files that have the
+specified values and tags.
+.IP
+.IP "\fBvadd [field=glob ...]\fP"
+Changes the current view, adding an additional level of directories
+to categorize the files.
+.IP
+For example, when the view is by author/tag, \fBvadd year=*\fP will 
+change it to year/author/tag. 
+.IP
+So will \fBvadd year=2014 year=2013\fP, but limiting the years in view
+to only those two.
+.IP
+.IP "\fBvcycle\fP"
+When a view involves nested subdirectories, this cycles the order.
+.IP
+For example, when the view is by year/author/tag, \fBvcycle\fP will switch
+it to author/tag/year.
+.IP
 .SH UTILITY COMMANDS
 .IP "\fBmigrate [path ...]\fP"
 .IP
@@ -694,7 +755,8 @@
 .IP
 Fixes up symlinks that are staged as part of a commit, to ensure they
 point to annexed content. Also handles injecting changes to unlocked
-files into the annex.
+files into the annex. When in a view, updates metadata to reflect changes
+made to files in the view.
 .IP
 .IP "\fBlookupkey [file ...]\fP"
 This plumbing\-level command looks up the key used for a file in the
@@ -963,6 +1025,10 @@
 The size can be specified with any commonly used units, for example,
 "0.5 gb" or "100 KiloBytes"
 .IP
+.IP "\fB\-\-metadata field=value\fP"
+Matches only files that have a metadata field attached with the specified
+value.
+.IP
 .IP "\fB\-\-want\-get\fP"
 Matches files that the preferred content settings for the repository
 make it want to get. Note that this will match even files that are
@@ -1385,6 +1451,15 @@
  *.flac annex.numcopies=3
 .PP
 Note that setting numcopies to 0 is very unsafe.
+.PP
+These settings are honored by git\-annex whenever it's operating on a
+matching file. However, when using \-\-all, \-\-unused, or \-\-key to specify
+keys to operate on, git\-annex is operating on keys and not files, so will
+not honor the settings from .gitattributes.
+.PP
+Also note that when using views, only the toplevel .gitattributes file is
+preserved in the view, so other settings in other files won't have any
+efffect.
 .PP
 .SH FILES
 These files are used by git\-annex:
diff --git a/git-annex.cabal b/git-annex.cabal
--- a/git-annex.cabal
+++ b/git-annex.cabal
@@ -1,5 +1,5 @@
 Name: git-annex
-Version: 5.20140210
+Version: 5.20140221
 Cabal-Version: >= 1.8
 License: GPL-3
 Maintainer: Joey Hess <joey@kitenet.net>
@@ -105,6 +105,7 @@
 
   if (os(windows))
     Build-Depends: Win32, Win32-extras
+    C-Sources: Utility/winprocess.c
   else
     Build-Depends: unix
     -- Need to list these because they're generated from .hsc files.
diff --git a/git-annex.hs b/git-annex.hs
--- a/git-annex.hs
+++ b/git-annex.hs
@@ -1,13 +1,13 @@
-{- git-annex main program stub
+{- git-annex main program dispatch
  -
- - Copyright 2010-2013 Joey Hess <joey@kitenet.net>
+ - Copyright 2010-2014 Joey Hess <joey@kitenet.net>
  -
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
 {-# LANGUAGE CPP #-}
 
-import System.Environment
+import System.Environment (getArgs, getProgName)
 import System.FilePath
 
 import qualified CmdLine.GitAnnex
@@ -16,19 +16,61 @@
 import qualified Test
 #endif
 
+#ifdef mingw32_HOST_OS
+import Utility.UserInfo
+import Utility.Env
+import Config.Files
+import System.Process
+import System.Exit
+#endif
+
 main :: IO ()
-main = run =<< getProgName
+main = do
+	ps <- getArgs
+	run ps =<< getProgName
   where
-	run n
-		| isshell n = go CmdLine.GitAnnexShell.run
-		| otherwise = go CmdLine.GitAnnex.run
-	isshell n = takeFileName n == "git-annex-shell"
-	go a = do
-		ps <- getArgs
+	run ps n
+		| isshell n = CmdLine.GitAnnexShell.run ps
+		| otherwise =
+#ifdef mingw32_HOST_OS
+			winEnv gitannex ps
+#else
+			gitannex ps
+#endif
+	gitannex ps = 
 #ifdef WITH_TESTSUITE
 		case ps of
 			("test":ps') -> Test.main ps'
-			_ -> a ps
+			_ -> CmdLine.GitAnnex.run ps
 #else
-		a ps
+		CmdLine.GitAnnex.run ps
+#endif
+	isshell n = takeFileName n == "git-annex-shell"
+
+#ifdef mingw32_HOST_OS
+{- On Windows, if HOME is not set, probe it and set it, re-execing
+ - git-annex with the new environment.
+ - 
+ - This is a workaround for some Cygwin commands needing HOME to be set,
+ - and for there being no known way to set environment variables on
+ - Windows, except by passing an environment in each call to a program.
+ - While ugly, this workaround is easier than trying to ensure HOME is set
+ - in all calls to the affected programs.
+ -}
+winEnv :: ([String] -> IO ()) -> [String] -> IO ()
+winEnv a ps = go =<< getEnv "HOME"
+  where
+	go (Just _) = a ps
+	go Nothing = do
+		home <- myHomeDir
+		putStrLn $ "** Windows hack; overrideing HOME to " ++ home
+		e <- getEnvironment
+		let eoverride =
+			[ ("HOME", home)
+			, ("CYGWIN", "nodosfilewarning")
+			]
+		cmd <- readProgramFile
+		(_, _, _, pid) <- createProcess (proc cmd ps)
+			{ env = Just $ e ++ eoverride }
+		exitWith =<< waitForProcess pid
 #endif
diff --git a/standalone/android/install-haskell-packages b/standalone/android/install-haskell-packages
--- a/standalone/android/install-haskell-packages
+++ b/standalone/android/install-haskell-packages
@@ -72,7 +72,6 @@
 	patched zlib
 	patched MissingH
 	patched bloomfilter
-	patched SafeSemaphore
 	patched distributive
 	patched comonad
 	patched iproute
@@ -86,6 +85,7 @@
 	patched skein
 	patched lens
 	patched certificate
+	patched x509-system
 	patched persistent-template
 	patched system-filepath
 	patched wai-app-static
diff --git a/templates/configurators/newrepository.hamlet b/templates/configurators/newrepository.hamlet
--- a/templates/configurators/newrepository.hamlet
+++ b/templates/configurators/newrepository.hamlet
@@ -7,6 +7,6 @@
     <a .btn .btn-primary href="@{AddDriveR}">
       Make a repository on a Removable Drive
   <p>
-    Where do you want to put this new repository?
+    Repository location:
     <form method="post" .form-inline enctype=#{enctype}>
       ^{form}
diff --git a/templates/configurators/newrepository/combine.hamlet b/templates/configurators/newrepository/combine.hamlet
--- a/templates/configurators/newrepository/combine.hamlet
+++ b/templates/configurators/newrepository/combine.hamlet
@@ -2,7 +2,7 @@
   <h2>
     Combine repositories?
   <p>
-    You have created a new repository at #{newrepo}.
+    The repository at #{newrepo} is set up.
     <br>
     Do you want to combine it with your existing repository at #{mainrepo}?
   <p>
