diff --git a/Annex.hs b/Annex.hs
--- a/Annex.hs
+++ b/Annex.hs
@@ -31,6 +31,7 @@
 	getGitConfig,
 	changeGitConfig,
 	changeGitRepo,
+	withCurrentState,
 ) where
 
 import "mtl" Control.Monad.State.Strict
@@ -216,3 +217,13 @@
 	{ repo = r
 	, gitconfig = extractGitConfig r
 	}
+
+{- Converts an Annex action into an IO action, that runs with a copy
+ - of the current Annex state. 
+ -
+ - Use with caution; the action should not rely on changing the
+ - state, as it will be thrown away. -}
+withCurrentState :: Annex a -> Annex (IO a)
+withCurrentState a = do
+	s <- getState id
+	return $ eval s a
diff --git a/Annex/Branch.hs b/Annex/Branch.hs
--- a/Annex/Branch.hs
+++ b/Annex/Branch.hs
@@ -76,8 +76,8 @@
 getBranch = maybe (hasOrigin >>= go >>= use) return =<< branchsha
   where
 	go True = do
-		inRepo $ Git.Command.run "branch"
-			[Param $ show name, Param $ show originname]
+		inRepo $ Git.Command.run
+			[Param "branch", Param $ show name, Param $ show originname]
 		fromMaybe (error $ "failed to create " ++ show name)
 			<$> branchsha
 	go False = withIndex' True $
diff --git a/Annex/Content.hs b/Annex/Content.hs
--- a/Annex/Content.hs
+++ b/Annex/Content.hs
@@ -276,6 +276,10 @@
  - In direct mode, it's possible for the file to change as it's being sent.
  - If this happens, runs the rollback action and returns False. The
  - rollback action should remove the data that was transferred.
+ -
+ - Note that the returned action is, in some cases, run in the Annex monad
+ - of the remote that is receiving the object, rather than the sender.
+ - So it cannot rely on Annex state, particular 
  -}
 sendAnnex :: Key -> Annex () -> (FilePath -> Annex Bool) -> Annex Bool
 sendAnnex key rollback sendobject = go =<< prepSendAnnex key
@@ -303,6 +307,7 @@
 	direct [] = return Nothing
 	direct (f:fs) = do
 		cache <- recordedInodeCache key
+		liftIO $ print ("prepSendAnnex pre cache", cache)
 		-- check that we have a good file
 		ifM (sameInodeCache f cache)
 			( return $ Just (f, sameInodeCache f cache)
diff --git a/Annex/Content/Direct.hs b/Annex/Content/Direct.hs
--- a/Annex/Content/Direct.hs
+++ b/Annex/Content/Direct.hs
@@ -13,6 +13,8 @@
 	recordedInodeCache,
 	updateInodeCache,
 	writeInodeCache,
+	compareInodeCaches,
+	compareInodeCachesWith,
 	sameInodeCache,
 	sameFileStatus,
 	removeInodeCache,
@@ -146,12 +148,19 @@
 {- If the inodes have changed, only the size and mtime are compared. -}
 compareInodeCaches :: InodeCache -> InodeCache -> Annex Bool
 compareInodeCaches x y
-	| x == y = return True
+	| compareStrong x y = return True
 	| otherwise = ifM inodesChanged
-		( return $ compareWeak x y
-		, return False
+		( do
+			liftIO $ print ("compareInodeCaches weak")
+			return $ compareWeak x y
+		, do
+			liftIO $ print ("compareInodeCaches no inode change but cache not match")
+			return False
 		)
 
+compareInodeCachesWith :: Annex InodeComparisonType
+compareInodeCachesWith = ifM inodesChanged ( return Weakly, return Strongly )
+
 {- Some filesystems get new inodes each time they are mounted.
  - In order to work on such a filesystem, a sentinal file is used to detect
  - when the inodes have changed.
@@ -166,9 +175,11 @@
 		scache <- liftIO . genInodeCache
 			=<< fromRepo gitAnnexInodeSentinal
 		scached <- readInodeSentinalFile
+		liftIO $ print (scache, scached)
 		let changed = case (scache, scached) of
-			(Just c1, Just c2) -> c1 /= c2
+			(Just c1, Just c2) -> not $ compareStrong c1 c2
 			_ -> True
+		liftIO $ print changed
 		Annex.changeState $ \s -> s { Annex.inodeschanged = Just changed }
 		return changed
 
diff --git a/Annex/Direct.hs b/Annex/Direct.hs
--- a/Annex/Direct.hs
+++ b/Annex/Direct.hs
@@ -51,8 +51,10 @@
 				 - modified, so compare the cache to see if
 				 - it really was. -}
 				oldcache <- recordedInodeCache key
-				when (oldcache /= Just cache) $
-					modifiedannexed file key cache
+				case oldcache of
+					Nothing -> modifiedannexed file key cache
+					Just c -> unlessM (compareInodeCaches c cache) $
+						modifiedannexed file key cache
 			(Just key, Nothing, _) -> deletedannexed file key
 			(Nothing, Nothing, _) -> deletegit file
 			(_, Just _, _) -> addgit file
diff --git a/Annex/Ssh.hs b/Annex/Ssh.hs
--- a/Annex/Ssh.hs
+++ b/Annex/Ssh.hs
@@ -33,7 +33,8 @@
 		liftIO $ createDirectoryIfMissing True $ parentDir socketfile
 		lockFile $ socket2lock socketfile
 		ret params
-	ret ps = return $ ps ++ opts ++ portParams port ++ [Param host]
+	ret ps = return $ ps ++ opts ++ portParams port ++
+		[Param "-T", Param host]
 	-- If the lock pool is empty, this is the first ssh of this
 	-- run. There could be stale ssh connections hanging around
 	-- from a previous git-annex run that was interrupted.
diff --git a/Annex/TaggedPush.hs b/Annex/TaggedPush.hs
new file mode 100644
--- /dev/null
+++ b/Annex/TaggedPush.hs
@@ -0,0 +1,57 @@
+{- git-annex tagged pushes
+ -
+ - Copyright 2012 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Annex.TaggedPush where
+
+import Common.Annex
+import qualified Remote
+import qualified Annex.Branch
+import qualified Git
+import qualified Git.Ref
+import qualified Git.Command
+import Utility.Base64
+
+{- Converts a git branch into a branch that is tagged with a UUID, typically
+ - the UUID of the repo that will be pushing it, and possibly with other
+ - information.
+ -
+ - Pushing to branches on the remote that have out uuid in them is ugly,
+ - but it reserves those branches for pushing by us, and so our pushes will
+ - never conflict with other pushes.
+ -
+ - To avoid cluttering up the branch display, the branch is put under
+ - refs/synced/, rather than the usual refs/remotes/
+ -
+ - Both UUIDs and Base64 encoded data are always legal to be used in git
+ - refs, per git-check-ref-format.
+ -}
+toTaggedBranch :: UUID -> Maybe String -> Git.Branch -> Git.Branch
+toTaggedBranch u info b = Git.Ref $ join "/" $ catMaybes
+	[ Just "refs/synced"
+	, Just $ fromUUID u
+	, toB64 <$> info
+	, Just $ show $ Git.Ref.base b
+	]
+
+fromTaggedBranch :: Git.Branch -> Maybe (UUID, Maybe String)
+fromTaggedBranch b = case split "/" $ show b of
+	("refs":"synced":u:info:_base) ->
+		Just (toUUID u, fromB64Maybe info)
+	("refs":"synced":u:_base) ->
+		Just (toUUID u, Nothing)
+	_ -> Nothing
+  where
+
+taggedPush :: UUID -> Maybe String -> Git.Ref -> Remote -> Git.Repo -> IO Bool
+taggedPush u info branch remote = Git.Command.runBool
+        [ Param "push"
+        , Param $ Remote.name remote
+        , Param $ refspec Annex.Branch.name
+        , Param $ refspec branch
+        ]
+  where
+	refspec b = show b ++ ":" ++ show (toTaggedBranch u info b)
diff --git a/Assistant.hs b/Assistant.hs
--- a/Assistant.hs
+++ b/Assistant.hs
@@ -133,7 +133,9 @@
 import Assistant.Threads.TransferWatcher
 import Assistant.Threads.Transferrer
 import Assistant.Threads.SanityChecker
+#ifdef WITH_CLIBS
 import Assistant.Threads.MountWatcher
+#endif
 import Assistant.Threads.NetWatcher
 import Assistant.Threads.TransferScanner
 import Assistant.Threads.TransferPoller
@@ -175,8 +177,8 @@
 				fdToHandle =<< dup stdOutput
 			origerr <- liftIO $ catchMaybeIO $ 
 				fdToHandle =<< dup stdError
-			liftIO $ Utility.Daemon.redirLog logfd
-			showStart (if assistant then "assistant" else "watch") "."
+			liftIO $ Utility.LogFile.redirLog logfd
+			showStart "." desc
 			start id $ 
 				case startbrowser of
 					Nothing -> Nothing
@@ -184,6 +186,9 @@
 		else
 			start (Utility.Daemon.daemonize logfd (Just pidfile) False) Nothing
   where
+  	desc
+		| assistant = "assistant"
+		| otherwise = "watch"
 	start daemonize webappwaiter = withThreadState $ \st -> do
 		checkCanWatch
 		when assistant $ checkEnvironment
@@ -217,8 +222,11 @@
 			, assist $ transferPollerThread
 			, assist $ transfererThread
 			, assist $ daemonStatusThread
-			, assist $ sanityCheckerThread
+			, assist $ sanityCheckerDailyThread
+			, assist $ sanityCheckerHourlyThread
+#ifdef WITH_CLIBS
 			, assist $ mountWatcherThread
+#endif
 			, assist $ netWatcherThread
 			, assist $ netWatcherFallbackThread
 			, assist $ transferScannerThread
diff --git a/Assistant/Alert.hs b/Assistant/Alert.hs
--- a/Assistant/Alert.hs
+++ b/Assistant/Alert.hs
@@ -5,7 +5,7 @@
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
-{-# LANGUAGE TemplateHaskell, QuasiQuotes, OverloadedStrings #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module Assistant.Alert where
 
@@ -313,6 +313,7 @@
 pairRequestAcknowledgedAlert who button = baseActivityAlert
 	{ alertData = ["Pairing with", UnTensed (T.pack who), Tensed "in progress" "complete"]
 	, alertPriority = High
+	, alertName = Just $ PairAlert who
 	, alertCombiner = Just $ dataCombiner $ \_old new -> new
 	, alertButton = button
 	}
diff --git a/Assistant/Changes.hs b/Assistant/Changes.hs
--- a/Assistant/Changes.hs
+++ b/Assistant/Changes.hs
@@ -12,9 +12,10 @@
 import Utility.TSet
 
 import Data.Time.Clock
+import Control.Concurrent.STM
 
 {- Handlers call this when they made a change that needs to get committed. -}
-madeChange :: FilePath -> ChangeType -> Assistant (Maybe Change)
+madeChange :: FilePath -> ChangeInfo -> Assistant (Maybe Change)
 madeChange f t = Just <$> (Change <$> liftIO getCurrentTime <*> pure f <*> pure t)
 
 noChange :: Assistant (Maybe Change)
@@ -27,13 +28,17 @@
 {- Gets all unhandled changes.
  - Blocks until at least one change is made. -}
 getChanges :: Assistant [Change]
-getChanges = getTSet <<~ changeChan
+getChanges = (atomically . getTSet) <<~ changeChan
 
+{- Gets all unhandled changes, without blocking. -}
+getAnyChanges :: Assistant [Change]
+getAnyChanges = (atomically . readTSet) <<~ changeChan
+
 {- Puts unhandled changes back into the channel.
  - Note: Original order is not preserved. -}
 refillChanges :: [Change] -> Assistant ()
-refillChanges cs = flip putTSet cs <<~ changeChan
+refillChanges cs = (atomically . flip putTSet cs) <<~ changeChan
 
 {- Records a change in the channel. -}
 recordChange :: Change -> Assistant ()
-recordChange c = flip putTSet1 c <<~ changeChan
+recordChange c = (atomically . flip putTSet1 c) <<~ changeChan
diff --git a/Assistant/Commits.hs b/Assistant/Commits.hs
--- a/Assistant/Commits.hs
+++ b/Assistant/Commits.hs
@@ -9,19 +9,20 @@
 
 import Assistant.Common
 import Assistant.Types.Commits
-
 import Utility.TSet
 
+import Control.Concurrent.STM
+
 {- Gets all unhandled commits.
  - Blocks until at least one commit is made. -}
 getCommits :: Assistant [Commit]
-getCommits = getTSet <<~ commitChan
+getCommits = (atomically . getTSet) <<~ commitChan
 
 {- Puts unhandled commits back into the channel.
  - Note: Original order is not preserved. -}
 refillCommits :: [Commit] -> Assistant ()
-refillCommits cs = flip putTSet cs <<~ commitChan
+refillCommits cs = (atomically . flip putTSet cs) <<~ commitChan
 
 {- Records a commit in the channel. -}
 recordCommit :: Assistant ()
-recordCommit = flip putTSet1 Commit <<~ commitChan
+recordCommit = (atomically . flip putTSet1 Commit) <<~ commitChan
diff --git a/Assistant/DaemonStatus.hs b/Assistant/DaemonStatus.hs
--- a/Assistant/DaemonStatus.hs
+++ b/Assistant/DaemonStatus.hs
@@ -43,7 +43,6 @@
 		sendNotification $ changeNotifier s
 		return b
 
-
 {- Returns a function that updates the lists of syncable remotes. -}
 calcSyncRemotes :: Annex (DaemonStatus -> DaemonStatus)
 calcSyncRemotes = do
diff --git a/Assistant/Drop.hs b/Assistant/Drop.hs
--- a/Assistant/Drop.hs
+++ b/Assistant/Drop.hs
@@ -21,23 +21,24 @@
 
 import qualified Data.Set as S
 
+type Reason = String
+
 {- Drop from local and/or remote when allowed by the preferred content and
  - numcopies settings. -}
-handleDrops :: Bool -> Key -> AssociatedFile -> Maybe Remote -> Assistant ()
-handleDrops _ _ Nothing _ = noop
-handleDrops fromhere key f knownpresentremote = do
+handleDrops :: Reason -> Bool -> Key -> AssociatedFile -> Maybe Remote -> Assistant ()
+handleDrops _ _ _ Nothing _ = noop
+handleDrops reason fromhere key f knownpresentremote = do
 	syncrs <- syncDataRemotes <$> getDaemonStatus
-	liftAnnex $ do
-		locs <- loggedLocations key
-		handleDropsFrom locs syncrs fromhere key f knownpresentremote
+	locs <- liftAnnex $ loggedLocations key
+	handleDropsFrom locs syncrs reason fromhere key f knownpresentremote
 
 {- The UUIDs are ones where the content is believed to be present.
  - The Remote list can include other remotes that do not have the content;
  - only ones that match the UUIDs will be dropped from.
  - If allows to drop fromhere, that drop will be tried first. -}
-handleDropsFrom :: [UUID] -> [Remote] -> Bool -> Key -> AssociatedFile -> Maybe Remote -> Annex ()
-handleDropsFrom _ _ _ _ Nothing _ = noop
-handleDropsFrom locs rs fromhere key (Just f) knownpresentremote
+handleDropsFrom :: [UUID] -> [Remote] -> Reason -> Bool -> Key -> AssociatedFile -> Maybe Remote -> Assistant ()
+handleDropsFrom _ _ _ _ _ Nothing _ = noop
+handleDropsFrom locs rs reason fromhere key (Just f) knownpresentremote
 	| fromhere = do
 		n <- getcopies
 		if checkcopies n
@@ -45,7 +46,7 @@
 			else go rs n
 	| otherwise = go rs =<< getcopies
   where
-	getcopies = do
+	getcopies = liftAnnex $ do
 		have <- length <$> trustExclude UnTrusted locs
 		numcopies <- getNumCopies =<< numCopies f
 		return (have, numcopies)
@@ -58,13 +59,22 @@
 		| checkcopies n = dropr r n >>= go rest
 		| otherwise = noop
 
-	checkdrop n@(_, numcopies) u a = ifM (wantDrop True u (Just f))
-		( ifM (safely $ doCommand $ a (Just numcopies))
-			( return $ decrcopies n
+	checkdrop n@(have, numcopies) u a =
+		ifM (liftAnnex $ wantDrop True u (Just f))
+			( ifM (liftAnnex $ safely $ doCommand $ a (Just numcopies))
+				( do
+					debug
+						[ "dropped"
+						, f
+						, "(from " ++ maybe "here" show u ++ ")"
+						, "(copies now " ++ show (have - 1) ++ ")"
+						, ": " ++ reason
+						]
+					return $ decrcopies n
+				, return n
+				)
 			, return n
 			)
-		, return n
-		)
 
 	dropl n = checkdrop n Nothing $ \numcopies ->
 		Command.Drop.startLocal f numcopies key knownpresentremote
diff --git a/Assistant/Install.hs b/Assistant/Install.hs
--- a/Assistant/Install.hs
+++ b/Assistant/Install.hs
@@ -15,6 +15,7 @@
 import Locations.UserConfig
 import Utility.FileMode
 import Utility.Shell
+import Utility.TempFile
 
 #ifdef darwin_HOST_OS
 import Utility.OSX
@@ -53,21 +54,25 @@
 		installAutoStart program autostartfile
 
 		{- This shim is only updated if it doesn't
-		 - already exist with the right content. This
-		 - ensures that there's no race where it would have
-		 - worked, but is unavailable due to being updated. -}
+		 - already exist with the right content. -}
 		sshdir <- sshDir
 		let shim = sshdir </> "git-annex-shell"
+		let runshell var = "exec " ++ base </> "runshell" ++
+			" git-annex-shell -c \"" ++ var ++ "\""
 		let content = unlines
 			[ shebang
 			, "set -e"
-			, "exec", base </> "runshell" ++ 
-			  " git-annex-shell -c \"$SSH_ORIGINAL_COMMAND\""
+			, "if [ \"x$SSH_ORIGINAL_COMMAND\" != \"x\" ]; then"
+			,   runshell "$SSH_ORIGINAL_COMMAND"
+			, "else"
+			,   runshell "$@"
+			, "fi"
 			]
+
 		curr <- catchDefaultIO "" $ readFileStrict shim
 		when (curr /= content) $ do
 			createDirectoryIfMissing True (parentDir shim)
-			writeFile shim content
+			viaTmp writeFile shim content
 			modifyFileMode shim $ addModes [ownerExecuteMode]
 
 {- Returns a cleaned up environment that lacks settings used to make the
diff --git a/Assistant/MakeRemote.hs b/Assistant/MakeRemote.hs
--- a/Assistant/MakeRemote.hs
+++ b/Assistant/MakeRemote.hs
@@ -20,15 +20,18 @@
 import Logs.UUID
 import Logs.Remote
 import Git.Remote
+import Config
+import Config.Cost
 
 import qualified Data.Text as T
 import qualified Data.Map as M
 
 {- Sets up and begins syncing with a new ssh or rsync remote. -}
-makeSshRemote :: Bool -> SshData -> Assistant Remote
-makeSshRemote forcersync sshdata = do
+makeSshRemote :: Bool -> SshData -> Maybe Cost -> Assistant Remote
+makeSshRemote forcersync sshdata mcost = do
 	r <- liftAnnex $
 		addRemote $ maker (sshRepoName sshdata) sshurl
+	liftAnnex $ maybe noop (setRemoteCost r) mcost
 	syncNewRemote r
 	return r
   where
@@ -52,7 +55,8 @@
 addRemote a = do
 	name <- a
 	void remoteListRefresh
-	maybe (error "failed to add remote") return =<< Remote.byName (Just name)
+	maybe (error "failed to add remote") return
+		=<< Remote.byName (Just name)
 
 {- Inits a rsync special remote, and returns its name. -}
 makeRsyncRemote :: String -> String -> Annex String
@@ -77,9 +81,8 @@
  - remote at the location, returns its name. -}
 makeGitRemote :: String -> String -> Annex String
 makeGitRemote basename location = makeRemote basename location $ \name ->
-	void $ inRepo $
-		Git.Command.runBool "remote"
-			[Param "add", Param name, Param location]
+	void $ inRepo $ Git.Command.runBool
+		[Param "remote", Param "add", Param name, Param location]
 
 {- If there's not already a remote at the location, adds it using the
  - action, which is passed the name of the remote to make.
diff --git a/Assistant/NetMessager.hs b/Assistant/NetMessager.hs
--- a/Assistant/NetMessager.hs
+++ b/Assistant/NetMessager.hs
@@ -15,6 +15,7 @@
 import Control.Concurrent.MSampleVar
 import Control.Exception as E
 import qualified Data.Set as S
+import qualified Data.Map as M
 
 sendNetMessage :: NetMessage -> Assistant ()
 sendNetMessage m = 
@@ -30,6 +31,42 @@
 waitNetMessagerRestart :: Assistant ()
 waitNetMessagerRestart = readSV <<~ (netMessagerRestart . netMessager)
 
+{- Store an important NetMessage for a client, and if the same message was
+ - already sent, remove it from sentImportantNetMessages. -}
+storeImportantNetMessage :: NetMessage -> ClientID -> (ClientID -> Bool) -> Assistant ()
+storeImportantNetMessage m client matchingclient = go <<~ netMessager
+  where
+	go nm = atomically $ do
+		q <- takeTMVar $ importantNetMessages nm
+		sent <- takeTMVar $ sentImportantNetMessages nm
+		putTMVar (importantNetMessages nm) $
+			M.alter (Just . maybe (S.singleton m) (S.insert m)) client q
+		putTMVar (sentImportantNetMessages nm) $
+			M.mapWithKey removematching sent
+	removematching someclient s
+		| matchingclient someclient = S.delete m s
+		| otherwise = s
+
+{- Indicates that an important NetMessage has been sent to a client. -}
+sentImportantNetMessage :: NetMessage -> ClientID -> Assistant ()
+sentImportantNetMessage m client = go <<~ (sentImportantNetMessages . netMessager)
+  where
+	go v = atomically $ do
+		sent <- takeTMVar v
+		putTMVar v $
+			M.alter (Just . maybe (S.singleton m) (S.insert m)) client sent
+
+{- Checks for important NetMessages that have been stored for a client, and
+ - sent to a client. Typically the same client for both, although 
+ - a modified or more specific client may need to be used. -}
+checkImportantNetMessages :: (ClientID, ClientID) -> Assistant (S.Set NetMessage, S.Set NetMessage)
+checkImportantNetMessages (storedclient, sentclient) = go <<~ netMessager
+  where
+	go nm = atomically $ do
+		stored <- M.lookup storedclient <$> (readTMVar $ importantNetMessages nm)
+		sent <- M.lookup sentclient <$> (readTMVar $ sentImportantNetMessages nm)
+		return (fromMaybe S.empty stored, fromMaybe S.empty sent)
+
 {- Runs an action that runs either the send or receive side of a push.
  -
  - While the push is running, netMessagesPush will get messages put into it
@@ -58,7 +95,7 @@
 
 {- While a push is running, matching push messages are put into
  - netMessagesPush, while others that involve the same side go to
- - netMessagesDeferredPush.
+ - netMessagesPushDeferred.
  -
  - When no push is running involving the same side, returns False.
  -
diff --git a/Assistant/Pairing/MakeRemote.hs b/Assistant/Pairing/MakeRemote.hs
--- a/Assistant/Pairing/MakeRemote.hs
+++ b/Assistant/Pairing/MakeRemote.hs
@@ -12,6 +12,7 @@
 import Assistant.Pairing
 import Assistant.Pairing.Network
 import Assistant.MakeRemote
+import Config.Cost
 
 import Network.Socket
 import qualified Data.Text as T
@@ -42,7 +43,7 @@
 			, "git-annex-shell -c configlist " ++ T.unpack (sshDirectory sshdata)
 			]
 			Nothing
-	void $ makeSshRemote False sshdata
+	void $ makeSshRemote False sshdata (Just semiExpensiveRemoteCost)
 
 {- Mostly a straightforward conversion.  Except:
  -  * Determine the best hostname to use to contact the host.
diff --git a/Assistant/Ssh.hs b/Assistant/Ssh.hs
--- a/Assistant/Ssh.hs
+++ b/Assistant/Ssh.hs
@@ -127,8 +127,13 @@
 	script =
 		[ shebang
 		, "set -e"
-		, "exec git-annex-shell -c \"$SSH_ORIGINAL_COMMAND\""
+		, "if [ \"x$SSH_ORIGINAL_COMMAND\" != \"x\" ]; then"
+		,   runshell "$SSH_ORIGINAL_COMMAND"
+		, "else"
+		,   runshell "$@"
+		, "fi"
 		]
+	runshell var = "exec git-annex-shell -c \"" ++ var ++ "\""
 
 authorizedKeysLine :: Bool -> FilePath -> SshPubKey -> String
 authorizedKeysLine rsynconly dir pubkey
diff --git a/Assistant/Sync.hs b/Assistant/Sync.hs
--- a/Assistant/Sync.hs
+++ b/Assistant/Sync.hs
@@ -18,15 +18,16 @@
 import Utility.Parallel
 import qualified Git
 import qualified Git.Branch
-import qualified Git.Ref
 import qualified Git.Command
 import qualified Remote
 import qualified Types.Remote as Remote
 import qualified Annex.Branch
 import Annex.UUID
+import Annex.TaggedPush
 
 import Data.Time.Clock
 import qualified Data.Map as M
+import qualified Data.Set as S
 import Control.Concurrent
 
 {- Syncs with remotes that may have been disconnected for a while.
@@ -36,17 +37,24 @@
  - An expensive full scan is queued when the git-annex branches of some of
  - the remotes have diverged from the local git-annex branch. Otherwise,
  - it's sufficient to requeue failed transfers.
+ -
+ - XMPP remotes are also signaled that we can push to them, and we request
+ - they push to us. Since XMPP pushes run ansynchronously, any scan of the
+ - XMPP remotes has to be deferred until they're done pushing to us, so
+ - all XMPP remotes are marked as possibly desynced.
  -}
 reconnectRemotes :: Bool -> [Remote] -> Assistant ()
 reconnectRemotes _ [] = noop
 reconnectRemotes notifypushes rs = void $ do
-	alertWhile (syncAlert rs) $ do
-		(ok, diverged) <- sync
-			=<< liftAnnex (inRepo Git.Branch.current)
-		addScanRemotes diverged rs
-		return ok
+	modifyDaemonStatus_ $ \s -> s
+		{ desynced = S.union (S.fromList $ map Remote.uuid xmppremotes) (desynced s) }
+	if null normalremotes
+		then go
+		else alertWhile (syncAlert normalremotes) go
   where
 	gitremotes = filter (notspecialremote . Remote.repo) rs
+	(xmppremotes, normalremotes) = partition isXMPPRemote gitremotes
+	nonxmppremotes = snd $ partition isXMPPRemote rs
 	notspecialremote r
 		| Git.repoIsUrl r = True
 		| Git.repoIsLocal r = True
@@ -54,12 +62,17 @@
 	sync (Just branch) = do
 		diverged <- snd <$> manualPull (Just branch) gitremotes
 		now <- liftIO getCurrentTime
-		ok <- pushToRemotes now notifypushes gitremotes
+		ok <- pushToRemotes' now notifypushes gitremotes
 		return (ok, diverged)
 	{- No local branch exists yet, but we can try pulling. -}
 	sync Nothing = do
 		diverged <- snd <$> manualPull Nothing gitremotes
 		return (True, diverged)
+	go = do
+		(ok, diverged) <- sync
+			=<< liftAnnex (inRepo Git.Branch.current)
+		addScanRemotes diverged nonxmppremotes
+		return ok
 
 {- Updates the local sync branch, then pushes it to all remotes, in
  - parallel, along with the git-annex branch. This is the same
@@ -84,8 +97,16 @@
  - reachable. If the fallback fails, the push is queued to be retried
  - later.
  -}
-pushToRemotes :: UTCTime -> Bool -> [Remote] -> Assistant Bool
-pushToRemotes now notifypushes remotes = do
+pushToRemotes :: Bool -> [Remote] -> Assistant Bool
+pushToRemotes notifypushes remotes = do
+	now <- liftIO $ getCurrentTime
+	let nonxmppremotes = snd $ partition isXMPPRemote remotes
+	let go = pushToRemotes' now notifypushes remotes
+	if null nonxmppremotes
+		then go
+		else alertWhile (pushAlert nonxmppremotes) go
+pushToRemotes' :: UTCTime -> Bool -> [Remote] -> Assistant Bool
+pushToRemotes' now notifypushes remotes = do
 	(g, branch, u) <- liftAnnex $ do
 		Annex.Branch.commit "update"
 		(,,)
@@ -128,7 +149,7 @@
 	fallback branch g u rs = do
 		debug ["fallback pushing to", show rs]
 		(succeeded, failed) <- liftIO $
-			inParallel (\r -> pushFallback u branch r g) rs
+			inParallel (\r -> taggedPush u Nothing branch r g) rs
 		updatemap succeeded failed
 		when (notifypushes && (not $ null succeeded)) $
 			sendNetMessage $ NotifyPush $
@@ -137,35 +158,25 @@
 		
 	push g branch remote = Command.Sync.pushBranch remote branch g
 
-{- This fallback push mode pushes to branches on the remote that have our
- - uuid in them. While ugly, those branches are reserved for pushing by us,
- - and so our pushes will never conflict with other pushes. -}
-pushFallback :: UUID -> Git.Ref -> Remote -> Git.Repo -> IO Bool
-pushFallback u branch remote = Git.Command.runBool "push" params
-  where
-	params = 
-		[ Param $ Remote.name remote
-		, Param $ refspec Annex.Branch.name
-		, Param $ refspec branch
-		]
-	{- Push to refs/synced/uuid/branch; this
-	 - avoids cluttering up the branch display. -}
-	refspec b = concat
-		[ s
-		, ":"
-		, "refs/synced/" ++ fromUUID u ++ "/" ++ s
-		]
-	  where s = show $ Git.Ref.base b
-
-{- Manually pull from remotes and merge their branches. -}
+{- Manually pull from remotes and merge their branches. Returns the results
+ - of all the pulls, and whether the git-annex branches of the remotes and
+ - local had divierged before the pull.
+ -
+ - After pulling from the normal git remotes, requests pushes from any XMPP
+ - remotes. However, those pushes will run asynchronously, so their
+ - results are not included in the return data.
+ -}
 manualPull :: Maybe Git.Ref -> [Remote] -> Assistant ([Bool], Bool)
 manualPull currentbranch remotes = do
 	g <- liftAnnex gitRepo
-	results <- liftIO $ forM remotes $ \r ->
-		Git.Command.runBool "fetch" [Param $ Remote.name r] g
+	let (xmppremotes, normalremotes) = partition isXMPPRemote remotes
+	results <- liftIO $ forM normalremotes $ \r ->
+		Git.Command.runBool [Param "fetch", Param $ Remote.name r] g
 	haddiverged <- liftAnnex Annex.Branch.forceUpdate
-	forM_ remotes $ \r ->
+	forM_ normalremotes $ \r ->
 		liftAnnex $ Command.Sync.mergeRemote r currentbranch
+	forM_ xmppremotes $ \r ->
+		sendNetMessage $ Pushing (getXMPPClientID r) PushRequest
 	return (results, haddiverged)
 
 {- Start syncing a newly added remote, using a background thread. -}
diff --git a/Assistant/Threads/Committer.hs b/Assistant/Threads/Committer.hs
--- a/Assistant/Threads/Committer.hs
+++ b/Assistant/Threads/Committer.hs
@@ -16,6 +16,7 @@
 import Assistant.Alert
 import Assistant.DaemonStatus
 import Assistant.TransferQueue
+import Assistant.Drop
 import Logs.Transfer
 import Logs.Location
 import qualified Annex.Queue
@@ -31,12 +32,17 @@
 import Annex.Exception
 import Annex.Content
 import Annex.Link
+import Annex.CatFile
 import qualified Annex
+import Utility.InodeCache
+import Annex.Content.Direct
 
 import Data.Time.Clock
 import Data.Tuple.Utils
 import qualified Data.Set as S
+import qualified Data.Map as M
 import Data.Either
+import Control.Concurrent
 
 {- This thread makes git commits at appropriate times. -}
 commitThread :: NamedThread
@@ -44,34 +50,105 @@
 	delayadd <- liftAnnex $
 		maybe delayaddDefault (return . Just . Seconds)
 			=<< annexDelayAdd <$> Annex.getGitConfig
-	runEvery (Seconds 1) <~> do
-		-- We already waited one second as a simple rate limiter.
-		-- Next, wait until at least one change is available for
-		-- processing.
-		changes <- getChanges
-		-- Now see if now's a good time to commit.
-		time <- liftIO getCurrentTime
-		if shouldCommit time changes
+	waitChangeTime $ \(changes, time) -> do
+		readychanges <- handleAdds delayadd changes
+		if shouldCommit time readychanges
 			then do
-				readychanges <- handleAdds delayadd changes
-				if shouldCommit time readychanges
-					then do
-						debug
-							[ "committing"
-							, show (length readychanges)
-							, "changes"
-							]
-						void $ alertWhile commitAlert $
-							liftAnnex commitStaged
-						recordCommit
-					else refill readychanges
-			else refill changes
+				debug
+					[ "committing"
+					, show (length readychanges)
+					, "changes"
+					]
+				void $ alertWhile commitAlert $
+					liftAnnex commitStaged
+				recordCommit
+				mapM_ checkChangeContent readychanges
+			else refill readychanges
+
+refill :: [Change] -> Assistant ()	
+refill [] = noop
+refill cs = do
+	debug ["delaying commit of", show (length cs), "changes"]
+	refillChanges cs
+
+{- Wait for one or more changes to arrive to be committed. -}
+waitChangeTime :: (([Change], UTCTime) -> Assistant ()) -> Assistant ()
+waitChangeTime a = runEvery (Seconds 1) <~> do
+	-- We already waited one second as a simple rate limiter.
+	-- Next, wait until at least one change is available for
+	-- processing.
+	changes <- getChanges
+	-- See if now's a good time to commit.
+	now <- liftIO getCurrentTime
+	case (shouldCommit now changes, possiblyrename changes) of
+		(True, False) -> a (changes, now)
+		(True, True) -> do
+			morechanges <- getrelatedchanges changes
+			a (changes ++ morechanges, now)
+		_ -> refill changes
   where
-	refill [] = noop
-	refill cs = do
-		debug ["delaying commit of", show (length cs), "changes"]
-		refillChanges cs
+	{- Did we perhaps only get one of the AddChange and RmChange pair
+	 - that make up a file rename? Or some of the pairs that make up 
+	 - a directory rename?
+	 -}
+	possiblyrename cs = all renamepart cs
 
+	renamepart (PendingAddChange _ _) = True
+	renamepart c = isRmChange c
+
+	{- Gets changes related to the passed changes, without blocking
+	 - very long.
+	 -
+	 - If there are multiple RmChanges, this is probably a directory
+	 - rename, in which case it may be necessary to wait longer to get
+	 - all the Changes involved.
+	 -}
+	getrelatedchanges oldchanges
+		| length (filter isRmChange oldchanges) > 1 =
+			concat <$> getbatchchanges []
+		| otherwise = do
+			liftIO humanImperceptibleDelay
+			getAnyChanges
+	getbatchchanges cs = do
+		liftIO $ threadDelay $ fromIntegral $ oneSecond `div` 10
+		cs' <- getAnyChanges
+		if null cs'
+			then return cs
+			else getbatchchanges (cs':cs)
+
+isRmChange :: Change -> Bool
+isRmChange (Change { changeInfo = i }) | i == RmChange = True
+isRmChange _ = False
+
+{- An amount of time that is hopefully imperceptably short for humans,
+ - while long enough for a computer to get some work done. 
+ - Note that 0.001 is a little too short for rename change batching to
+ - work. -}
+humanImperceptibleInterval :: NominalDiffTime
+humanImperceptibleInterval = 0.01
+
+humanImperceptibleDelay :: IO ()
+humanImperceptibleDelay = threadDelay $
+	truncate $ humanImperceptibleInterval * fromIntegral oneSecond
+
+{- Decide if now is a good time to make a commit.
+ - Note that the list of changes has an undefined order.
+ -
+ - Current strategy: If there have been 10 changes within the past second,
+ - a batch activity is taking place, so wait for later.
+ -}
+shouldCommit :: UTCTime -> [Change] -> Bool
+shouldCommit now changes
+	| len == 0 = False
+	| len > 10000 = True -- avoid bloating queue too much
+	| length recentchanges < 10 = True
+	| otherwise = False -- batch activity
+  where
+	len = length changes
+	thissecond c = timeDelta c <= 1
+	recentchanges = filter thissecond changes
+	timeDelta c = now `diffUTCTime` changeTime c
+
 commitStaged :: Annex Bool
 commitStaged = do
 	{- This could fail if there's another commit being made by
@@ -81,8 +158,7 @@
 		Left _ -> return False
 		Right _ -> do
 			direct <- isDirect
-			void $ inRepo $ Git.Command.runBool "commit" $ nomessage $
-				catMaybes
+			let params = nomessage $ catMaybes
 				[ Just $ Param "--quiet"
 				{- In indirect mode, avoid running the
 				 - usual git-annex pre-commit hook;
@@ -94,6 +170,8 @@
 			{- Empty commits may be made if tree changes cancel
 			 - each other out, etc. Git returns nonzero on those,
 			 - so don't propigate out commit failures. -}
+			void $ inRepo $ catchMaybeIO . 
+				Git.Command.runQuiet (Param "commit" : params)
 			return True
   where
 	nomessage ps
@@ -102,22 +180,6 @@
 		| otherwise = Param "--allow-empty-message" 
 			: Param "-m" : Param "" : ps
 
-{- Decide if now is a good time to make a commit.
- - Note that the list of change times has an undefined order.
- -
- - Current strategy: If there have been 10 changes within the past second,
- - a batch activity is taking place, so wait for later.
- -}
-shouldCommit :: UTCTime -> [Change] -> Bool
-shouldCommit now changes
-	| len == 0 = False
-	| len > 10000 = True -- avoid bloating queue too much
-	| length (filter thisSecond changes) < 10 = True
-	| otherwise = False -- batch activity
-  where
-	len = length changes
-	thisSecond c = now `diffUTCTime` changeTime c <= 1
-
 {- OSX needs a short delay after a file is added before locking it down,
  - when using a non-direct mode repository, as pasting a file seems to
  - try to set file permissions or otherwise access the file after closing
@@ -165,7 +227,9 @@
 		refillChanges postponed
 
 	returnWhen (null toadd) $ do
-		added <- catMaybes <$> forM toadd add
+		added <- catMaybes <$> if direct
+			then adddirect toadd
+			else forM toadd add
 		if DirWatcher.eventsCoalesce || null added || direct
 			then return $ added ++ otherchanges
 			else do
@@ -195,7 +259,7 @@
 					key <- liftAnnex $ do
 						showStart "add" $ keyFilename ks
 						Command.Add.ingest $ Just ks
-					done (finishedChange change) (keyFilename ks) key
+					maybe failedingest (done change $ keyFilename ks) key
 	  where
 		{- Add errors tend to be transient and will be automatically
 		 - dealt with, so don't pass to the alert code. -}
@@ -203,22 +267,59 @@
 		ret _ = (True, Nothing)
 	add _ = return Nothing
 
-	done _ _ Nothing = do
+	{- In direct mode, avoid overhead of re-injesting a renamed
+	 - file, by examining the other Changes to see if a removed
+	 - file has the same InodeCache as the new file. If so,
+	 - we can just update bookkeeping, and stage the file in git.
+	 -}
+	adddirect :: [Change] -> Assistant [Maybe Change]
+	adddirect toadd = do
+		ct <- liftAnnex compareInodeCachesWith
+		m <- liftAnnex $ removedKeysMap ct cs
+		if M.null m
+			then forM toadd add
+			else forM toadd $ \c -> do
+				mcache <- liftIO $ genInodeCache $ changeFile c
+				case mcache of
+					Nothing -> add c
+					Just cache ->
+						case M.lookup (inodeCacheToKey ct cache) m of
+							Nothing -> add c
+							Just k -> fastadd c k
+
+	fastadd :: Change -> Key -> Assistant (Maybe Change)
+	fastadd change key = do
+		let source = keySource change
+		liftAnnex $ Command.Add.finishIngestDirect key source
+		done change (keyFilename source) key
+
+	removedKeysMap :: InodeComparisonType -> [Change] -> Annex (M.Map InodeCacheKey Key)
+	removedKeysMap ct l = do
+		mks <- forM (filter isRmChange l) $ \c ->
+			catKeyFile $ changeFile c
+		M.fromList . catMaybes <$> forM (catMaybes mks) mkpair
+	  where
+		mkpair k = do
+			mcache <- recordedInodeCache k
+			case mcache of
+				Just cache -> return $ Just (inodeCacheToKey ct cache, k)
+				Nothing -> return Nothing
+
+	failedingest = do
 		liftAnnex showEndFail
 		return Nothing
-	done change file (Just key) = do
-		liftAnnex $ do
-			logStatus key InfoPresent
-			link <- ifM isDirect
-				( calcGitLink file key
-				, Command.Add.link file key True
-				)
-			whenM (pure DirWatcher.eventsCoalesce <||> isDirect) $ do
-				stageSymlink file =<< hashSymlink link
-				showEndOk
-		queueTransfers Next key (Just file) Upload
-		return $ Just change
 
+	done change file key = liftAnnex $ do
+		logStatus key InfoPresent
+		link <- ifM isDirect
+			( calcGitLink file key
+			, Command.Add.link file key True
+			)
+		whenM (pure DirWatcher.eventsCoalesce <||> isDirect) $ do
+			stageSymlink file =<< hashSymlink link
+			showEndOk
+		return $ Just $ finishedChange change key
+
 	{- Check that the keysource's keyFilename still exists,
 	 - and is still a hard link to its contentLocation,
 	 - before ingesting it. -}
@@ -298,3 +399,23 @@
 			tmpdir <- fromRepo gitAnnexTmpDir
 			liftIO $ Lsof.queryDir tmpdir
 		)
+
+{- After a Change is committed, queue any necessary transfers or drops
+ - of the content of the key.
+ -
+ - This is not done during the startup scan, because the expensive
+ - transfer scan does the same thing then.
+ -}
+checkChangeContent :: Change -> Assistant ()
+checkChangeContent change@(Change { changeInfo = i }) =
+	case changeInfoKey i of
+		Nothing -> noop
+		Just k -> whenM (scanComplete <$> getDaemonStatus) $ do
+			present <- liftAnnex $ inAnnex k
+			if present
+				then queueTransfers "new file created" Next k (Just f) Upload
+				else queueTransfers "new or renamed file wanted" Next k (Just f) Download
+			handleDrops "file renamed" present k (Just f) Nothing
+  where
+	f = changeFile change
+checkChangeContent _ = noop
diff --git a/Assistant/Threads/Glacier.hs b/Assistant/Threads/Glacier.hs
--- a/Assistant/Threads/Glacier.hs
+++ b/Assistant/Threads/Glacier.hs
@@ -39,5 +39,5 @@
 		let l' = filter (\p -> S.member (getkey p) s) l
 		forM_ l' $ \(t, info) -> do
 			liftAnnex $ removeFailedTransfer t
-			queueTransferWhenSmall (associatedFile info) t r
+			queueTransferWhenSmall "object available from glacier" (associatedFile info) t r
 	getkey = transferKey . fst
diff --git a/Assistant/Threads/Merger.hs b/Assistant/Threads/Merger.hs
--- a/Assistant/Threads/Merger.hs
+++ b/Assistant/Threads/Merger.hs
@@ -10,13 +10,20 @@
 import Assistant.Common
 import Assistant.TransferQueue
 import Assistant.BranchChange
+import Assistant.DaemonStatus
+import Assistant.ScanRemotes
 import Utility.DirWatcher
-import Utility.Types.DirWatcher
+import Utility.DirWatcher.Types
 import qualified Annex.Branch
 import qualified Git
 import qualified Git.Branch
 import qualified Command.Sync
+import Annex.TaggedPush
+import Remote (remoteFromUUID)
 
+import qualified Data.Set as S
+import qualified Data.Text as T
+
 {- This thread watches for changes to .git/refs/, and handles incoming
  - pushes. -}
 mergeThread :: NamedThread
@@ -64,13 +71,16 @@
 	| ".lock" `isSuffixOf` file = noop
 	| isAnnexBranch file = do
 		branchChanged
-		whenM (liftAnnex Annex.Branch.forceUpdate) $
-			queueDeferredDownloads Later
+		diverged <- liftAnnex Annex.Branch.forceUpdate
+		when diverged $
+			unlessM handleDesynced $
+				queueDeferredDownloads "retrying deferred download" Later
 	| "/synced/" `isInfixOf` file = do
 		mergecurrent =<< liftAnnex (inRepo Git.Branch.current)
 	| otherwise = noop
   where
 	changedbranch = fileToBranch file
+
 	mergecurrent (Just current)
 		| equivBranches changedbranch current = do
 			debug
@@ -79,6 +89,22 @@
 				]
 			void $ liftAnnex  $ Command.Sync.mergeFrom changedbranch
 	mergecurrent _ = noop
+
+	handleDesynced = case fromTaggedBranch changedbranch of
+		Nothing -> return False
+		Just (u, info) -> do
+			mr <- liftAnnex $ remoteFromUUID u
+			case mr of
+				Nothing -> return False
+				Just r -> do
+					s <- desynced <$> getDaemonStatus
+					if S.member u s || Just (T.unpack $ getXMPPClientID r) == info
+						then do
+							modifyDaemonStatus_ $ \st -> st
+								{ desynced = S.delete u s }
+							addScanRemotes True [r]
+							return True
+						else return False
 
 equivBranches :: Git.Ref -> Git.Ref -> Bool
 equivBranches x y = base x == base y
diff --git a/Assistant/Threads/PairListener.hs b/Assistant/Threads/PairListener.hs
--- a/Assistant/Threads/PairListener.hs
+++ b/Assistant/Threads/PairListener.hs
@@ -11,7 +11,7 @@
 import Assistant.Pairing
 import Assistant.Pairing.Network
 import Assistant.Pairing.MakeRemote
-import Assistant.WebApp
+import Assistant.WebApp (UrlRenderer, renderUrl)
 import Assistant.WebApp.Types
 import Assistant.Alert
 import Assistant.DaemonStatus
diff --git a/Assistant/Threads/Pusher.hs b/Assistant/Threads/Pusher.hs
--- a/Assistant/Threads/Pusher.hs
+++ b/Assistant/Threads/Pusher.hs
@@ -17,8 +17,6 @@
 import Utility.ThreadScheduler
 import qualified Types.Remote as Remote
 
-import Data.Time.Clock
-
 {- This thread retries pushes that failed before. -}
 pushRetryThread :: NamedThread
 pushRetryThread = namedThread "PushRetrier" $ runEvery (Seconds halfhour) <~> do
@@ -27,9 +25,8 @@
 	topush <- getFailedPushesBefore (fromIntegral halfhour)
 	unless (null topush) $ do
 		debug ["retrying", show (length topush), "failed pushes"]
-		void $ alertWhile (pushRetryAlert topush) $ do
-			now <- liftIO $ getCurrentTime
-			pushToRemotes now True topush
+		void $ alertWhile (pushRetryAlert topush) $
+			pushToRemotes True topush
   where
 	halfhour = 1800
 
@@ -41,16 +38,21 @@
 	commits <- getCommits
 	-- Now see if now's a good time to push.
 	if shouldPush commits
-		then do
-			remotes <- filter (not . Remote.readonly) 
-				. syncGitRemotes <$> getDaemonStatus
-			unless (null remotes) $
-				void $ alertWhile (pushAlert remotes) $ do
-					now <- liftIO $ getCurrentTime
-					pushToRemotes now True remotes
+		then void $ pushToRemotes True =<< pushTargets
 		else do
 			debug ["delaying push of", show (length commits), "commits"]
 			refillCommits commits
+
+{- We want to avoid pushing to remotes that are marked readonly.
+ -
+ - Also, avoid pushing to local remotes we can easily tell are not available,
+ - to avoid ugly messages when a removable drive is not attached.
+ -}
+pushTargets :: Assistant [Remote]
+pushTargets = liftIO . filterM available =<< candidates <$> getDaemonStatus
+  where
+	candidates = filter (not . Remote.readonly) . syncGitRemotes
+	available = maybe (return True) doesDirectoryExist . Remote.localpath
 
 {- Decide if now is a good time to push to remotes.
  -
diff --git a/Assistant/Threads/SanityChecker.hs b/Assistant/Threads/SanityChecker.hs
--- a/Assistant/Threads/SanityChecker.hs
+++ b/Assistant/Threads/SanityChecker.hs
@@ -6,21 +6,32 @@
  -}
 
 module Assistant.Threads.SanityChecker (
-	sanityCheckerThread
+	sanityCheckerDailyThread,
+	sanityCheckerHourlyThread
 ) where
 
 import Assistant.Common
 import Assistant.DaemonStatus
 import Assistant.Alert
 import qualified Git.LsFiles
+import qualified Git.Command
+import qualified Git.Config
 import Utility.ThreadScheduler
 import qualified Assistant.Threads.Watcher as Watcher
+import Utility.LogFile
+import Config
 
 import Data.Time.Clock.POSIX
 
-{- This thread wakes up occasionally to make sure the tree is in good shape. -}
-sanityCheckerThread :: NamedThread
-sanityCheckerThread = namedThread "SanityChecker" $ forever $ do
+{- This thread wakes up hourly for inxepensive frequent sanity checks. -}
+sanityCheckerHourlyThread :: NamedThread
+sanityCheckerHourlyThread = namedThread "SanityCheckerHourly" $ forever $ do
+	liftIO $ threadDelaySeconds $ Seconds oneHour
+	hourlyCheck
+
+{- This thread wakes up daily to make sure the tree is in good shape. -}
+sanityCheckerDailyThread :: NamedThread
+sanityCheckerDailyThread = namedThread "SanityCheckerDaily" $ forever $ do
 	waitForNextCheck
 
 	debug ["starting sanity check"]
@@ -31,7 +42,7 @@
 		modifyDaemonStatus_ $ \s -> s { sanityCheckRunning = True }
 
 		now <- liftIO $ getPOSIXTime -- before check started
-		r <- either showerr return =<< tryIO <~> check
+		r <- either showerr return =<< tryIO <~> dailyCheck
 
 		modifyDaemonStatus_ $ \s -> s
 			{ sanityCheckRunning = False
@@ -57,15 +68,13 @@
 			oneDay - truncate (now - lastcheck)
 		| otherwise = oneDay
 
-oneDay :: Int
-oneDay = 24 * 60 * 60
-
 {- It's important to stay out of the Annex monad as much as possible while
  - running potentially expensive parts of this check, since remaining in it
  - will block the watcher. -}
-check :: Assistant Bool
-check = do
+dailyCheck :: Assistant Bool
+dailyCheck = do
 	g <- liftAnnex gitRepo
+
 	-- Find old unstaged symlinks, and add them to git.
 	(unstaged, cleanup) <- liftIO $ Git.LsFiles.notInRepo False ["."] g
 	now <- liftIO $ getPOSIXTime
@@ -76,6 +85,19 @@
 				| isSymbolicLink s -> addsymlink file ms
 			_ -> noop
 	liftIO $ void cleanup
+
+	{- Allow git-gc to run once per day. More frequent gc is avoided
+	 - by default to avoid slowing things down. Only run repacks when 100x
+	 - the usual number of loose objects are present; we tend
+	 - to have a lot of small objects and they should not be a
+	 - significant size. -}
+	when (Git.Config.getMaybe "gc.auto" g == Just "0") $
+		liftIO $ void $ Git.Command.runBool
+			[ Param "-c", Param "gc.auto=670000"
+			, Param "gc"
+			, Param "--auto"
+			] g
+
 	return True
   where
 	toonew timestamp now = now < (realToFrac (timestamp + slop) :: POSIXTime)
@@ -84,5 +106,32 @@
 		liftAnnex $ warning msg
 		void $ addAlert $ sanityCheckFixAlert msg
 	addsymlink file s = do
-		Watcher.runHandler Watcher.onAddSymlink file s
+		isdirect <- liftAnnex isDirect
+		Watcher.runHandler (Watcher.onAddSymlink isdirect) file s
 		insanity $ "found unstaged symlink: " ++ file
+
+hourlyCheck :: Assistant ()
+hourlyCheck = checkLogSize 0
+
+{- Rotate logs until log file size is < 1 mb. -}
+checkLogSize :: Int -> Assistant ()
+checkLogSize n = do
+	f <- liftAnnex $ fromRepo gitAnnexLogFile
+	logs <- liftIO $ listLogs f
+	totalsize <- liftIO $ sum <$> mapM filesize logs
+	when (totalsize > oneMegabyte) $ do
+		notice ["Rotated logs due to size:", show totalsize]
+		liftIO $ openLog f >>= redirLog
+		when (n < maxLogs + 1) $
+			checkLogSize $ n + 1
+  where
+	filesize f = fromIntegral . fileSize <$> liftIO (getFileStatus f)
+
+oneMegabyte :: Int
+oneMegabyte = 1000000
+
+oneHour :: Int
+oneHour = 60 * 60
+
+oneDay :: Int
+oneDay = 24 * oneHour
diff --git a/Assistant/Threads/TransferScanner.hs b/Assistant/Threads/TransferScanner.hs
--- a/Assistant/Threads/TransferScanner.hs
+++ b/Assistant/Threads/TransferScanner.hs
@@ -14,6 +14,7 @@
 import Assistant.DaemonStatus
 import Assistant.Alert
 import Assistant.Drop
+import Assistant.Sync
 import Logs.Transfer
 import Logs.Location
 import Logs.Web (webUUID)
@@ -45,11 +46,14 @@
 			else do
 				mapM_ failedTransferScan rs
 				go scanned
-	{- All available remotes are scanned in full on startup,
-	 - for multiple reasons, including:
+	{- All git remotes are synced, and all available remotes
+	 - are scanned in full on startup, for multiple reasons, including:
 	 -
 	 - * This may be the first run, and there may be remotes
 	 -   already in place, that need to be synced.
+	 - * Changes may have been made last time we run, but remotes were
+	 -   not available to be synced with.
+	 - * Changes may have been made to remotes while we were down.
 	 - * We may have run before, and scanned a remote, but
 	 -   only been in a subdirectory of the git remote, and so
 	 -   not synced it all.
@@ -57,7 +61,9 @@
 	 -   and then the system (or us) crashed, and that info was
 	 -   lost.
 	 -}
-	startupScan = addScanRemotes True =<< syncDataRemotes <$> getDaemonStatus
+	startupScan = do
+		reconnectRemotes True =<< syncGitRemotes <$> getDaemonStatus
+		addScanRemotes True =<< syncDataRemotes <$> getDaemonStatus
 
 {- This is a cheap scan for failed transfers involving a remote. -}
 failedTransferScan :: Remote -> Assistant ()
@@ -78,7 +84,7 @@
 			 - that the remote doesn't already have the
 			 - key, so it's not redundantly checked here. -}
 			requeue t info
-	requeue t info = queueTransferWhenSmall (associatedFile info) t r
+	requeue t info = queueTransferWhenSmall "retrying failed transfer" (associatedFile info) t r
 
 {- This is a expensive scan through the full git work tree, finding
  - files to transfer. The scan is blocked when the transfer queue gets
@@ -108,19 +114,19 @@
 	onlyweb = all (== webUUID) $ map Remote.uuid rs
 	visiblers = let rs' = filter (not . Remote.readonly) rs
 		in if null rs' then rs else rs'
-	enqueue f (r, t) = do
-		debug ["queuing", show t]
-		queueTransferWhenSmall (Just f) t r
+	enqueue f (r, t) =
+		queueTransferWhenSmall "expensive scan found missing object"
+			(Just f) t r
 	findtransfers f (key, _) = do
 		{- The syncable remotes may have changed since this
 		 - scan began. -}
 		syncrs <- syncDataRemotes <$> getDaemonStatus
+		locs <- liftAnnex $ loggedLocations key
+		present <- liftAnnex $ inAnnex key
+		handleDropsFrom locs syncrs
+			"expensive scan found too many copies of object"
+			present key (Just f) Nothing
 		liftAnnex $ do
-			locs <- loggedLocations key
-			present <- inAnnex key
-
-			handleDropsFrom locs syncrs present key (Just f) Nothing
-
 			let slocs = S.fromList locs
 			let use a = return $ catMaybes $ map (a key slocs) syncrs
 			if present
diff --git a/Assistant/Threads/TransferWatcher.hs b/Assistant/Threads/TransferWatcher.hs
--- a/Assistant/Threads/TransferWatcher.hs
+++ b/Assistant/Threads/TransferWatcher.hs
@@ -14,7 +14,7 @@
 import Annex.Content
 import Logs.Transfer
 import Utility.DirWatcher
-import Utility.Types.DirWatcher
+import Utility.DirWatcher.Types
 import qualified Remote
 
 import Control.Concurrent
@@ -61,7 +61,7 @@
   where
 	go _ Nothing = noop -- transfer already finished
 	go t (Just info) = do
-		debug [ "transfer starting:", show t]
+		debug [ "transfer starting:", describeTransfer t info ]
 		r <- headMaybe . filter (sameuuid t)
 			<$> liftAnnex Remote.remoteList
 		updateTransferInfo t info { transferRemote = r }
@@ -115,9 +115,14 @@
 finishedTransfer t (Just info)
 	| transferDirection t == Download =
 		whenM (liftAnnex $ inAnnex $ transferKey t) $ do
-			handleDrops False (transferKey t) (associatedFile info) Nothing
-			queueTransfersMatching (/= transferUUID t) Later
-				(transferKey t) (associatedFile info) Upload
-	| otherwise = handleDrops True (transferKey t) (associatedFile info) Nothing
+			dodrops False
+			queueTransfersMatching (/= transferUUID t)
+				"newly received object"
+				Later (transferKey t) (associatedFile info) Upload
+	| otherwise = dodrops True
+  where
+	dodrops fromhere = handleDrops
+		("drop wanted after " ++ describeTransfer t info)
+		fromhere (transferKey t) (associatedFile info) Nothing
 finishedTransfer _ _ = noop
 
diff --git a/Assistant/Threads/Transferrer.hs b/Assistant/Threads/Transferrer.hs
--- a/Assistant/Threads/Transferrer.hs
+++ b/Assistant/Threads/Transferrer.hs
@@ -20,6 +20,8 @@
 import qualified Remote
 import Types.Key
 import Locations.UserConfig
+import Assistant.Threads.TransferWatcher
+import Annex.Wanted
 
 import System.Process (create_group)
 
@@ -34,18 +36,24 @@
 	{- Skip transfers that are already running. -}
 	notrunning = isNothing . startedTime
 
-{- By the time this is called, the daemonstatus's transfer map should
+{- By the time this is called, the daemonstatus's currentTransfers map should
  - already have been updated to include the transfer. -}
-startTransfer :: FilePath -> Transfer -> TransferInfo -> Assistant (Maybe (Transfer, TransferInfo, Assistant ()))
+startTransfer
+	:: FilePath
+	-> Transfer
+	-> TransferInfo
+	-> Assistant (Maybe (Transfer, TransferInfo, Assistant ()))
 startTransfer program t info = case (transferRemote info, associatedFile info) of
 	(Just remote, Just file) -> ifM (liftAnnex $ shouldTransfer t info)
 		( do
-			debug [ "Transferring:" , show t ]
+			debug [ "Transferring:" , describeTransfer t info ]
 			notifyTransfer
 			return $ Just (t, info, transferprocess remote file)
 		, do
-			debug [ "Skipping unnecessary transfer:" , show t ]
+			debug [ "Skipping unnecessary transfer:",
+				describeTransfer t info ]
 			void $ removeTransfer t
+			finishedTransfer t (Just info)
 			return Nothing
 		)
 	_ -> return Nothing
@@ -55,8 +63,9 @@
 
 	transferprocess remote file = void $ do
 		(_, _, _, pid)
-			<- liftIO $ createProcess (proc program $ toCommand params)
-				{ create_group = True }
+			<- liftIO $ createProcess
+				(proc program $ toCommand params)
+					{ create_group = True }
 		{- Alerts are only shown for successful transfers.
 		 - Transfers can temporarily fail for many reasons,
 		 - so there's no point in bothering the user about
@@ -76,7 +85,9 @@
 			void $ addAlert $ makeAlertFiller True $
 				transferFileAlert direction True file
 			unless isdownload $
-				handleDrops True (transferKey t)
+				handleDrops
+					("object uploaded to " ++ show remote)
+					True (transferKey t)
 					(associatedFile info)
 					(Just remote)
 			recordCommit
@@ -93,21 +104,40 @@
 			, File file
 			]
 
-{- Checks if the file to download is already present, or the remote
- - being uploaded to isn't known to have the file. -}
+{- Called right before a transfer begins, this is a last chance to avoid
+ - unnecessary transfers.
+ -
+ - For downloads, we obviously don't need to download if the already
+ - have the object.
+ -
+ - Smilarly, for uploads, check if the remote is known to already have
+ - the object.
+ -
+ - Also, uploads get queued to all remotes, in order of cost.
+ - This may mean, for example, that an object is uploaded over the LAN
+ - to a locally paired client, and once that upload is done, a more
+ - expensive transfer remote no longer wants the object. (Since
+ - all the clients have it already.) So do one last check if this is still
+ - preferred content.
+ -
+ - We'll also do one last preferred content check for downloads. An
+ - example of a case where this could be needed is if a download is queued
+ - for a file that gets moved out of an archive directory -- but before
+ - that download can happen, the file is put back in the archive.
+ -}
 shouldTransfer :: Transfer -> TransferInfo -> Annex Bool
 shouldTransfer t info
 	| transferDirection t == Download =
-		not <$> inAnnex key
-	| transferDirection t == Upload =
-		{- Trust the location log to check if the
-		 - remote already has the key. This avoids
-		 - a roundtrip to the remote. -}
-		case transferRemote info of
-			Nothing -> return False
-			Just remote -> 
-				notElem (Remote.uuid remote)
-					<$> loggedLocations key
+		(not <$> inAnnex key) <&&> wantGet True file
+	| transferDirection t == Upload = case transferRemote info of
+		Nothing -> return False
+		Just r -> notinremote r
+			<&&> wantSend True file (Remote.uuid r)
 	| otherwise = return False
   where
 	key = transferKey t
+	file = associatedFile info
+
+	{- Trust the location log to check if the remote already has
+	 - the key. This avoids a roundtrip to the remote. -}
+	notinremote r = notElem (Remote.uuid r) <$> loggedLocations key
diff --git a/Assistant/Threads/Watcher.hs b/Assistant/Threads/Watcher.hs
--- a/Assistant/Threads/Watcher.hs
+++ b/Assistant/Threads/Watcher.hs
@@ -20,12 +20,9 @@
 import Assistant.DaemonStatus
 import Assistant.Changes
 import Assistant.Types.Changes
-import Assistant.TransferQueue
 import Assistant.Alert
-import Assistant.Drop
-import Logs.Transfer
 import Utility.DirWatcher
-import Utility.Types.DirWatcher
+import Utility.DirWatcher.Types
 import Utility.Lsof
 import qualified Annex
 import qualified Annex.Queue
@@ -46,6 +43,7 @@
 import Data.Typeable
 import qualified Data.ByteString.Lazy as L
 import qualified Control.Exception as E
+import Data.Time.Clock
 
 checkCanWatch :: Annex ()
 checkCanWatch
@@ -82,7 +80,7 @@
 	direct <- liftAnnex isDirect
 	addhook <- hook $ if direct then onAddDirect else onAdd
 	delhook <- hook onDel
-	addsymlinkhook <- hook onAddSymlink
+	addsymlinkhook <- hook $ onAddSymlink direct
 	deldirhook <- hook onDelDir
 	errhook <- hook onErr
 	let hooks = mkWatchHooks
@@ -178,6 +176,7 @@
  - really been modified. -}
 onAddDirect :: Handler
 onAddDirect file fs = do
+	debug ["add direct", file]
 	v <- liftAnnex $ catKeyFile file
 	case (v, fs) of
 		(Just key, Just filestatus) ->
@@ -193,25 +192,24 @@
  - Or, if it is a git-annex symlink, ensure it points to the content
  - before adding it.
  -}
-onAddSymlink :: Handler
-onAddSymlink file filestatus = go =<< liftAnnex (Backend.lookupFile file)
+onAddSymlink :: Bool -> Handler
+onAddSymlink isdirect file filestatus = go =<< liftAnnex (Backend.lookupFile file)
   where
 	go (Just (key, _)) = do
+		when isdirect $
+			liftAnnex $ void $ addAssociatedFile key file
 		link <- liftAnnex $ calcGitLink file key
 		ifM ((==) (Just link) <$> liftIO (catchMaybeIO $ readSymbolicLink file))
-			( do
-				s <- getDaemonStatus
-				checkcontent key s
-				ensurestaged (Just link) s
+			( ensurestaged (Just link) (Just key) =<< getDaemonStatus
 			, do
-				liftIO $ removeFile file
-				liftAnnex $ Backend.makeAnnexLink link file
-				checkcontent key =<< getDaemonStatus
-				addlink link
+				unless isdirect $ do
+					liftIO $ removeFile file
+					liftAnnex $ Backend.makeAnnexLink link file
+				addlink link (Just key)
 			)
 	go Nothing = do -- other symlink
 		mlink <- liftIO (catchMaybeIO $ readSymbolicLink file)
-		ensurestaged mlink =<< getDaemonStatus
+		ensurestaged mlink Nothing =<< getDaemonStatus
 
 	{- This is often called on symlinks that are already
 	 - staged correctly. A symlink may have been deleted
@@ -224,16 +222,16 @@
 	 - (If the daemon has never ran before, avoid staging
 	 - links too.)
 	 -}
-	ensurestaged (Just link) daemonstatus
-		| scanComplete daemonstatus = addlink link
+	ensurestaged (Just link) mk daemonstatus
+		| scanComplete daemonstatus = addlink link mk
 		| otherwise = case filestatus of
 			Just s
 				| not (afterLastDaemonRun (statusChangeTime s) daemonstatus) -> noChange
-			_ -> addlink link
-	ensurestaged Nothing _ = noChange
+			_ -> addlink link mk
+	ensurestaged Nothing _ _ = noChange
 
 	{- For speed, tries to reuse the existing blob for symlink target. -}
-	addlink link = do
+	addlink link mk = do
 		debug ["add symlink", file]
 		liftAnnex $ do
 			v <- catObjectDetails $ Ref $ ':':file
@@ -242,20 +240,7 @@
 					| s2w8 link == L.unpack currlink ->
 						stageSymlink file sha
 				_ -> stageSymlink file =<< hashSymlink link
-		madeChange file LinkChange
-
-	{- When a new link appears, or a link is changed, after the startup
-	 - scan, handle getting or dropping the key's content.
-	 - Also, moving or copying a link may caused it be be transferred
-	 - elsewhere, so check that too. -}
-	checkcontent key daemonstatus
-		| scanComplete daemonstatus = do
-			present <- liftAnnex $ inAnnex key
-			if present
-				then queueTransfers Next key (Just file) Upload
-				else queueTransfers Next key (Just file) Download
-			handleDrops present key (Just file) Nothing
-		| otherwise = noop
+		madeChange file $ LinkChange mk
 
 onDel :: Handler
 onDel file _ = do
@@ -267,17 +252,26 @@
 
 {- A directory has been deleted, or moved, so tell git to remove anything
  - that was inside it from its cache. Since it could reappear at any time,
- - use --cached to only delete it from the index. 
+ - use --cached to only delete it from the index.
  -
- - Note: This could use unstageFile, but would need to run another git
- - command to get the recursive list of files in the directory, so rm is
- - just as good. -}
+ - This queues up a lot of RmChanges, which assists the Committer in
+ - pairing up renamed files when the directory was renamed. -}
 onDelDir :: Handler
 onDelDir dir _ = do
 	debug ["directory deleted", dir]
-	liftAnnex $ Annex.Queue.addCommand "rm"
-		[Params "--quiet -r --cached --ignore-unmatch --"] [dir]
-	madeChange dir RmDirChange
+	(fs, clean) <- liftAnnex $ inRepo $ LsFiles.deleted [dir]
+
+	liftAnnex $ forM_ fs $ \f -> Annex.Queue.addUpdateIndex =<<
+		inRepo (Git.UpdateIndex.unstageFile f)
+
+	-- Get the events queued up as fast as possible, so the
+	-- committer sees them all in one block.
+	now <- liftIO getCurrentTime
+	forM_ fs $ \f -> recordChange $ Change now f RmChange
+
+	void $ liftIO $ clean
+	liftAnnex $ Annex.Queue.flushWhenFull
+	noChange
 
 {- Called when there's an error with inotify or kqueue. -}
 onErr :: Handler
diff --git a/Assistant/Threads/WebApp.hs b/Assistant/Threads/WebApp.hs
--- a/Assistant/Threads/WebApp.hs
+++ b/Assistant/Threads/WebApp.hs
@@ -16,6 +16,7 @@
 import Assistant.WebApp.DashBoard
 import Assistant.WebApp.SideBar
 import Assistant.WebApp.Notifications
+import Assistant.WebApp.RepoList
 import Assistant.WebApp.Configurators
 import Assistant.WebApp.Configurators.Edit
 import Assistant.WebApp.Configurators.Local
@@ -24,6 +25,7 @@
 import Assistant.WebApp.Configurators.AWS
 import Assistant.WebApp.Configurators.WebDAV
 import Assistant.WebApp.Configurators.XMPP
+import Assistant.WebApp.Configurators.Preferences
 import Assistant.WebApp.Documentation
 import Assistant.WebApp.Control
 import Assistant.WebApp.OtherRepos
@@ -86,6 +88,6 @@
 		maybe noop (\a -> a url htmlshim) onstartup
 
 myUrl :: WebApp -> SockAddr -> Url
-myUrl webapp addr = unpack $ yesodRender webapp urlbase HomeR []
+myUrl webapp addr = unpack $ yesodRender webapp urlbase DashboardR []
   where
 	urlbase = pack $ "http://" ++ show addr
diff --git a/Assistant/Threads/XMPPClient.hs b/Assistant/Threads/XMPPClient.hs
--- a/Assistant/Threads/XMPPClient.hs
+++ b/Assistant/Threads/XMPPClient.hs
@@ -33,23 +33,27 @@
 import qualified Git.Branch
 import Data.Time.Clock
 
+{- Whether to include verbose protocol dump in debug output. -}
+protocolDebug :: Bool
+protocolDebug = False
+
 xmppClientThread :: UrlRenderer -> NamedThread
 xmppClientThread urlrenderer = namedThread "XMPPClient" $
 	restartableClient . xmppClient urlrenderer =<< getAssistant id
 
 {- Runs the client, handing restart events. -}
-restartableClient :: IO () -> Assistant ()
-restartableClient a = forever $ do
-	tid <- liftIO $ forkIO a
-	waitNetMessagerRestart
-	liftIO $ killThread tid
+restartableClient :: (XMPPCreds -> IO ()) -> Assistant ()
+restartableClient a = forever $ go =<< liftAnnex getXMPPCreds
+  where
+	go Nothing = waitNetMessagerRestart
+	go (Just creds) = do
+		tid <- liftIO $ forkIO $ a creds
+		waitNetMessagerRestart
+		liftIO $ killThread tid
 
-xmppClient :: UrlRenderer -> AssistantData -> IO ()
-xmppClient urlrenderer d = do
-	v <- liftAssistant $ liftAnnex getXMPPCreds
-	case v of
-		Nothing -> noop -- will be restarted once creds get configured
-		Just c -> retry (runclient c) =<< getCurrentTime
+xmppClient :: UrlRenderer -> AssistantData -> XMPPCreds -> IO ()
+xmppClient urlrenderer d creds =
+	retry (runclient creds) =<< getCurrentTime
   where
 	liftAssistant = runAssistant d
 	inAssistant = liftIO . liftAssistant
@@ -58,7 +62,14 @@
 	 - if it keeps failing, back off to wait 5 minutes before
 	 - trying it again. -}
 	retry client starttime = do
+		{- The buddy list starts empty each time
+		 - the client connects, so that stale info
+		 - is not retained. -}
+		liftAssistant $
+			updateBuddyList (const noBuddies) <<~ buddyList
 		e <- client
+		liftAssistant $ modifyDaemonStatus_ $ \s -> s
+			{ xmppClientID = Nothing }
 		now <- getCurrentTime
 		if diffUTCTime now starttime > 300
 			then do
@@ -73,12 +84,10 @@
 		selfjid <- bindJID jid
 		putStanza gitAnnexSignature
 
-		inAssistant $ debug ["connected", show selfjid]
-		{- The buddy list starts empty each time
-		 - the client connects, so that stale info
-		 - is not retained. -}
-		void $ inAssistant $
-			updateBuddyList (const noBuddies) <<~ buddyList
+		inAssistant $ do
+			modifyDaemonStatus_ $ \s -> s
+				{ xmppClientID = Just $ xmppJID creds }
+			debug ["connected", show selfjid]
 
 		xmppThread $ receivenotifications selfjid
 		forever $ do
@@ -87,11 +96,14 @@
 
 	receivenotifications selfjid = forever $ do
 		l <- decodeStanza selfjid <$> getStanza
-		-- inAssistant $ debug ["received:", show l]
+		when protocolDebug $
+			inAssistant $ debug ["received:", show l]
 		mapM_ (handle selfjid) l
 
-	handle _ (PresenceMessage p) = void $ inAssistant $ 
-		updateBuddyList (updateBuddies p) <<~ buddyList
+	handle selfjid (PresenceMessage p) = do
+		void $ inAssistant $ 
+			updateBuddyList (updateBuddies p) <<~ buddyList
+		resendImportantMessages selfjid p
 	handle _ (GotNetMessage QueryPresence) = putStanza gitAnnexSignature
 	handle _ (GotNetMessage (NotifyPush us)) = void $ inAssistant $ pull us
 	handle selfjid (GotNetMessage (PairingNotification stage c u)) =
@@ -105,6 +117,16 @@
 	handle _ (Unknown _) = noop
 	handle _ (ProtocolError _) = noop
 
+	resendImportantMessages selfjid (Presence { presenceFrom = Just jid }) = do
+		let c = formatJID jid
+		(stored, sent) <- inAssistant $
+			checkImportantNetMessages (formatJID (baseJID jid), c)
+		forM_ (S.toList $ S.difference stored sent) $ \msg -> do
+			inAssistant $ debug ["sending to new client:", show c, show msg]
+			a <- inAssistant $ convertNetMsg (readdressNetMessage msg c) selfjid
+			a
+			inAssistant $ sentImportantNetMessage msg c
+	resendImportantMessages _ _ = noop
 
 data XMPPEvent
 	= GotNetMessage NetMessage
@@ -142,29 +164,59 @@
  - Chat messages must be directed to specific clients, not a base
  - account JID, due to git-annex clients using a negative presence priority.
  - PairingNotification messages are always directed at specific
- - clients, but Pushing messages are sometimes not, and need to be exploded.
+ - clients, but Pushing messages are sometimes not, and need to be exploded
+ - out to specific clients.
+ -
+ - Important messages, not directed at any specific client, 
+ - are cached to be sent later when additional clients connect.
  -}
 relayNetMessage :: JID -> Assistant (XMPP ())
-relayNetMessage selfjid = convert =<< waitNetMessage
+relayNetMessage selfjid = do
+	msg <- waitNetMessage
+	when protocolDebug $
+		debug ["sending:", show msg]
+	handleImportant msg
+	convert msg
   where
-	convert (NotifyPush us) = return $ putStanza $ pushNotification us
-	convert QueryPresence = return $ putStanza presenceQuery
-	convert (PairingNotification stage c u) = withclient c $ \tojid -> do
-		changeBuddyPairing tojid True
-		return $ putStanza $ pairingNotification stage u tojid selfjid
-	convert (Pushing c pushstage) = withclient c $ \tojid -> do
+	handleImportant msg = case parseJID =<< isImportantNetMessage msg of
+		Just tojid
+			| tojid == baseJID tojid ->
+				storeImportantNetMessage msg (formatJID tojid) $
+					\c -> (baseJID <$> parseJID c) == Just tojid
+		_ -> noop
+	convert (Pushing c pushstage) = withOtherClient selfjid c $ \tojid -> do
 		if tojid == baseJID tojid
 			then do
-				bud <- getBuddy (genBuddyKey tojid) <<~ buddyList
-				return $ forM_ (maybe [] (S.toList . buddyAssistants) bud) $ \(Client jid) ->
+				clients <- maybe [] (S.toList . buddyAssistants)
+					<$> getBuddy (genBuddyKey tojid) <<~ buddyList
+				when protocolDebug $
+					debug ["exploded undirected message to clients", show clients]
+				return $ forM_ (clients) $ \(Client jid) ->
 					putStanza $ pushMessage pushstage jid selfjid
 			else return $ putStanza $ pushMessage pushstage tojid selfjid
+	convert msg = convertNetMsg msg selfjid
 
-	withclient c a = case parseJID c of
-		Nothing -> return noop
-		Just tojid
-			| tojid == selfjid -> return noop
-			| otherwise -> a tojid
+{- Converts a NetMessage to an XMPP action. -}
+convertNetMsg :: NetMessage -> JID -> Assistant (XMPP ())
+convertNetMsg msg selfjid = convert msg
+  where
+	convert (NotifyPush us) = return $ putStanza $ pushNotification us
+	convert QueryPresence = return $ putStanza presenceQuery
+	convert (PairingNotification stage c u) = withOtherClient selfjid c $ \tojid -> do
+		changeBuddyPairing tojid True
+		return $ putStanza $ pairingNotification stage u tojid selfjid
+	convert (Pushing c pushstage) = withOtherClient selfjid c $ \tojid ->
+		return $ putStanza $  pushMessage pushstage tojid selfjid
+
+withOtherClient :: JID -> ClientID -> (JID -> Assistant (XMPP ())) -> (Assistant (XMPP ()))
+withOtherClient selfjid c a = case parseJID c of
+	Nothing -> return noop
+	Just tojid
+		| tojid == selfjid -> return noop
+		| otherwise -> a tojid
+
+withClient :: ClientID -> (JID -> XMPP ()) -> XMPP ()
+withClient c a = maybe noop a $ parseJID c
 
 {- Runs a XMPP action in a separate thread, using a session to allow it
  - to access the same XMPP client. -}
diff --git a/Assistant/TransferQueue.hs b/Assistant/TransferQueue.hs
--- a/Assistant/TransferQueue.hs
+++ b/Assistant/TransferQueue.hs
@@ -33,6 +33,8 @@
 import Control.Concurrent.STM
 import qualified Data.Map as M
 
+type Reason = String
+
 {- Reads the queue's content without blocking or changing it. -}
 getTransferQueue :: Assistant [(Transfer, TransferInfo)]
 getTransferQueue = (atomically . readTVar . queuelist) <<~ transferQueue
@@ -45,25 +47,25 @@
 
 {- Adds transfers to queue for some of the known remotes.
  - Honors preferred content settings, only transferring wanted files. -}
-queueTransfers :: Schedule -> Key -> AssociatedFile -> Direction -> Assistant ()
+queueTransfers :: Reason -> Schedule -> Key -> AssociatedFile -> Direction -> Assistant ()
 queueTransfers = queueTransfersMatching (const True)
 
 {- Adds transfers to queue for some of the known remotes, that match a
  - condition. Honors preferred content settings. -}
-queueTransfersMatching :: (UUID -> Bool) -> Schedule -> Key -> AssociatedFile -> Direction -> Assistant ()
-queueTransfersMatching matching schedule k f direction
+queueTransfersMatching :: (UUID -> Bool) -> Reason -> Schedule -> Key -> AssociatedFile -> Direction -> Assistant ()
+queueTransfersMatching matching reason schedule k f direction
 	| direction == Download = whenM (liftAnnex $ wantGet True f) go
 	| otherwise = go
   where
 	go = do
-		rs <- liftAnnex . sufficientremotes
+		rs <- liftAnnex . selectremotes
 			=<< syncDataRemotes <$> getDaemonStatus
 		let matchingrs = filter (matching . Remote.uuid) rs
 		if null matchingrs
 			then defer
 			else forM_ matchingrs $ \r ->
-				enqueue schedule (gentransfer r) (stubInfo f r)
-	sufficientremotes rs
+				enqueue reason schedule (gentransfer r) (stubInfo f r)
+	selectremotes rs
 		{- Queue downloads from all remotes that
 		 - have the key, with the cheapest ones first.
 		 - More expensive ones will only be tried if
@@ -90,8 +92,8 @@
 
 {- Queues any deferred downloads that can now be accomplished, leaving
  - any others in the list to try again later. -}
-queueDeferredDownloads :: Schedule -> Assistant ()
-queueDeferredDownloads schedule = do
+queueDeferredDownloads :: Reason -> Schedule -> Assistant ()
+queueDeferredDownloads reason schedule = do
 	q <- getAssistant transferQueue
 	l <- liftIO $ atomically $ swapTVar (deferreddownloads q) []
 	rs <- syncDataRemotes <$> getDaemonStatus
@@ -105,7 +107,8 @@
 		let sources = filter (\r -> uuid r `elem` uuids) rs
 		unless (null sources) $
 			forM_ sources $ \r ->
-				enqueue schedule (gentransfer r) (stubInfo f r)
+				enqueue reason schedule 
+					(gentransfer r) (stubInfo f r)
 		return $ null sources
 	  where
 		gentransfer r = Transfer
@@ -114,42 +117,50 @@
 			, transferUUID = Remote.uuid r
 			}
 
-enqueue :: Schedule -> Transfer -> TransferInfo -> Assistant ()
-enqueue schedule t info
+enqueue :: Reason -> Schedule -> Transfer -> TransferInfo -> Assistant ()
+enqueue reason schedule t info
 	| schedule == Next = go (new:)
 	| otherwise = go (\l -> l++[new])
   where
 	new = (t, info)
-	go modlist = do
+	go modlist = whenM (add modlist) $ do
+		debug [ "queued", describeTransfer t info, ": " ++ reason ]
+		notifyTransfer
+	add modlist = do
 		q <- getAssistant transferQueue
 		liftIO $ atomically $ do
-			void $ modifyTVar' (queuesize q) succ
-			void $ modifyTVar' (queuelist q) modlist
-		notifyTransfer
+			l <- readTVar (queuelist q)
+			if (new `notElem` l)
+				then do	
+					void $ modifyTVar' (queuesize q) succ
+					void $ modifyTVar' (queuelist q) modlist
+					return True
+				else return False
 
 {- Adds a transfer to the queue. -}
-queueTransfer :: Schedule -> AssociatedFile -> Transfer -> Remote -> Assistant ()
-queueTransfer schedule f t remote = enqueue schedule t (stubInfo f remote)
+queueTransfer :: Reason -> Schedule -> AssociatedFile -> Transfer -> Remote -> Assistant ()
+queueTransfer reason schedule f t remote =
+	enqueue reason schedule t (stubInfo f remote)
 
 {- Blocks until the queue is no larger than a given size, and then adds a
  - transfer to the queue. -}
-queueTransferAt :: Int -> Schedule -> AssociatedFile -> Transfer -> Remote -> Assistant ()
-queueTransferAt wantsz schedule f t remote = do
+queueTransferAt :: Int -> Reason -> Schedule -> AssociatedFile -> Transfer -> Remote -> Assistant ()
+queueTransferAt wantsz reason schedule f t remote = do
 	q <- getAssistant transferQueue
 	liftIO $ atomically $ do
 		sz <- readTVar (queuesize q)
 		unless (sz <= wantsz) $
 			retry -- blocks until queuesize changes
-	enqueue schedule t (stubInfo f remote)
+	enqueue reason schedule t (stubInfo f remote)
 
-queueTransferWhenSmall :: AssociatedFile -> Transfer -> Remote -> Assistant ()
-queueTransferWhenSmall = queueTransferAt 10 Later
+queueTransferWhenSmall :: Reason -> AssociatedFile -> Transfer -> Remote -> Assistant ()
+queueTransferWhenSmall reason = queueTransferAt 10 reason Later
 
 {- Blocks until a pending transfer is available in the queue,
  - and removes it.
  -
  - Checks that it's acceptable, before adding it to the
- - the currentTransfers map. If it's not acceptable, it's discarded.
+ - currentTransfers map. If it's not acceptable, it's discarded.
  -
  - This is done in a single STM transaction, so there is no window
  - where an observer sees an inconsistent status. -}
diff --git a/Assistant/Types/Changes.hs b/Assistant/Types/Changes.hs
--- a/Assistant/Types/Changes.hs
+++ b/Assistant/Types/Changes.hs
@@ -8,24 +8,34 @@
 module Assistant.Types.Changes where
 
 import Types.KeySource
+import Types.Key
 import Utility.TSet
 
 import Data.Time.Clock
+import Control.Concurrent.STM
 
-data ChangeType = AddChange | LinkChange | RmChange | RmDirChange
+data ChangeInfo = AddChange Key | LinkChange (Maybe Key) | RmChange
 	deriving (Show, Eq)
 
+changeInfoKey :: ChangeInfo -> Maybe Key
+changeInfoKey (AddChange k) = Just k
+changeInfoKey (LinkChange (Just k)) = Just k
+changeInfoKey _ = Nothing
+
 type ChangeChan = TSet Change
 
+newChangeChan :: IO ChangeChan
+newChangeChan = atomically newTSet
+
 data Change
 	= Change 
 		{ changeTime :: UTCTime
-		, changeFile :: FilePath
-		, changeType :: ChangeType
+		, _changeFile :: FilePath
+		, changeInfo :: ChangeInfo
 		}
 	| PendingAddChange
 		{ changeTime ::UTCTime
-		, changeFile :: FilePath
+		, _changeFile :: FilePath
 		}
 	| InProcessAddChange
 		{ changeTime ::UTCTime
@@ -33,8 +43,10 @@
 		}
 	deriving (Show)
 
-newChangeChan :: IO ChangeChan
-newChangeChan = newTSet
+changeFile :: Change -> FilePath
+changeFile (Change _ f _) = f
+changeFile (PendingAddChange _ f) = f
+changeFile (InProcessAddChange _ ks) = keyFilename ks
 
 isPendingAddChange :: Change -> Bool
 isPendingAddChange (PendingAddChange {}) = True
@@ -44,11 +56,10 @@
 isInProcessAddChange (InProcessAddChange {}) = True
 isInProcessAddChange _ = False
 
-finishedChange :: Change -> Change
-finishedChange c@(InProcessAddChange { keySource = ks }) = Change
+finishedChange :: Change -> Key -> Change
+finishedChange c@(InProcessAddChange { keySource = ks }) k = Change
 	{ changeTime = changeTime c
-	, changeFile = keyFilename ks
-	, changeType = AddChange
+	, _changeFile = keyFilename ks
+	, changeInfo = AddChange k
 	}
-finishedChange c = c
-
+finishedChange c _ = c
diff --git a/Assistant/Types/Commits.hs b/Assistant/Types/Commits.hs
--- a/Assistant/Types/Commits.hs
+++ b/Assistant/Types/Commits.hs
@@ -9,9 +9,11 @@
 
 import Utility.TSet
 
+import Control.Concurrent.STM
+
 type CommitChan = TSet Commit
 
 data Commit = Commit
 
 newCommitChan :: IO CommitChan
-newCommitChan = newTSet
+newCommitChan = atomically newTSet
diff --git a/Assistant/Types/DaemonStatus.hs b/Assistant/Types/DaemonStatus.hs
--- a/Assistant/Types/DaemonStatus.hs
+++ b/Assistant/Types/DaemonStatus.hs
@@ -15,11 +15,13 @@
 import Utility.NotificationBroadcaster
 import Logs.Transfer
 import Assistant.Types.ThreadName
+import Assistant.Types.NetMessager
 
 import Control.Concurrent.STM
 import Control.Concurrent.Async
 import Data.Time.Clock.POSIX
 import qualified Data.Map as M
+import qualified Data.Set as S
 
 data DaemonStatus = DaemonStatus
 	-- All the named threads that comprise the daemon,
@@ -44,6 +46,8 @@
 	, syncGitRemotes :: [Remote]
 	-- Ordered list of remotes to sync data with
 	, syncDataRemotes :: [Remote]
+	-- List of uuids of remotes that we may have gotten out of sync with.
+	, desynced :: S.Set UUID
 	-- Pairing request that is in progress.
 	, pairingInProgress :: Maybe PairingInProgress
 	-- Broadcasts notifications about all changes to the DaemonStatus
@@ -54,6 +58,9 @@
 	, alertNotifier :: NotificationBroadcaster
 	-- Broadcasts notifications when the syncRemotes change
 	, syncRemotesNotifier :: NotificationBroadcaster
+	-- When the XMPP client is connected, this will contain the XMPP
+	-- address.
+	, xmppClientID :: Maybe ClientID
 	}
 
 type TransferMap = M.Map Transfer TransferInfo
@@ -74,8 +81,10 @@
 	<*> pure []
 	<*> pure []
 	<*> pure []
+	<*> pure S.empty
 	<*> pure Nothing
 	<*> newNotificationBroadcaster
 	<*> newNotificationBroadcaster
 	<*> newNotificationBroadcaster
 	<*> newNotificationBroadcaster
+	<*> pure Nothing
diff --git a/Assistant/Types/NetMessager.hs b/Assistant/Types/NetMessager.hs
--- a/Assistant/Types/NetMessager.hs
+++ b/Assistant/Types/NetMessager.hs
@@ -15,6 +15,7 @@
 import Control.Concurrent.MSampleVar
 import Data.ByteString (ByteString)
 import qualified Data.Set as S
+import qualified Data.Map as M
 
 {- Messages that can be sent out of band by a network messager. -}
 data NetMessage 
@@ -47,6 +48,18 @@
 	| ReceivePackDone ExitCode
 	deriving (Show, Eq, Ord)
 
+{- NetMessages that are important (and small), and should be stored to be
+ - resent when new clients are seen. -}
+isImportantNetMessage :: NetMessage -> Maybe ClientID
+isImportantNetMessage (Pushing c CanPush) = Just c
+isImportantNetMessage (Pushing c PushRequest) = Just c
+isImportantNetMessage _ = Nothing
+
+readdressNetMessage :: NetMessage -> ClientID -> NetMessage
+readdressNetMessage (PairingNotification stage _ uuid) c = PairingNotification stage c uuid
+readdressNetMessage (Pushing _ stage) c = Pushing c stage
+readdressNetMessage m _ = m
+
 {- Things that initiate either side of a push, but do not actually send data. -}
 isPushInitiation :: PushStage -> Bool
 isPushInitiation CanPush = True
@@ -81,6 +94,10 @@
 data NetMessager = NetMessager
 	-- outgoing messages
 	{ netMessages :: TChan (NetMessage)
+	-- important messages for each client
+	, importantNetMessages :: TMVar (M.Map ClientID (S.Set NetMessage))
+	-- important messages that are believed to have been sent to a client
+	, sentImportantNetMessages :: TMVar (M.Map ClientID (S.Set NetMessage))
 	-- write to this to restart the net messager
 	, netMessagerRestart :: MSampleVar ()
 	-- only one side of a push can be running at a time
@@ -94,8 +111,9 @@
 newNetMessager :: IO NetMessager
 newNetMessager = NetMessager
 	<$> atomically newTChan
+	<*> atomically (newTMVar M.empty)
+	<*> atomically (newTMVar M.empty)
 	<*> newEmptySV
 	<*> mkSideMap (newTMVar Nothing)
 	<*> mkSideMap newTChan
 	<*> mkSideMap (newTMVar S.empty)
-  where
diff --git a/Assistant/Types/ScanRemotes.hs b/Assistant/Types/ScanRemotes.hs
--- a/Assistant/Types/ScanRemotes.hs
+++ b/Assistant/Types/ScanRemotes.hs
@@ -13,7 +13,7 @@
 import qualified Data.Map as M
 
 data ScanInfo = ScanInfo
-	{ scanPriority :: Int
+	{ scanPriority :: Float
 	, fullScan :: Bool
 	}
 
diff --git a/Assistant/WebApp.hs b/Assistant/WebApp.hs
--- a/Assistant/WebApp.hs
+++ b/Assistant/WebApp.hs
@@ -10,7 +10,8 @@
 module Assistant.WebApp where
 
 import Assistant.WebApp.Types
-import Assistant.Common
+import Assistant.Common hiding (liftAnnex)
+import qualified Assistant.Monad as Assistant
 import Utility.NotificationBroadcaster
 import Utility.Yesod
 
@@ -25,9 +26,6 @@
 newWebAppState :: IO (TMVar WebAppState)
 newWebAppState = atomically $ newTMVar $ WebAppState { showIntro = True }
 
-liftAssistant :: forall sub a. (Assistant a) -> GHandler sub WebApp a
-liftAssistant a = liftIO . flip runAssistant a =<< assistantData <$> getYesod
-
 getWebAppState :: forall sub. GHandler sub WebApp WebAppState
 getWebAppState = liftIO . atomically . readTMVar =<< webAppState <$> getYesod
 
@@ -43,12 +41,18 @@
  - When the webapp is run outside a git-annex repository, the fallback
  - value is returned.
  -}
-runAnnex :: forall sub a. a -> Annex a -> GHandler sub WebApp a
-runAnnex fallback a = ifM (noAnnex <$> getYesod)
+liftAnnexOr :: forall sub a. a -> Annex a -> GHandler sub WebApp a
+liftAnnexOr fallback a = ifM (noAnnex <$> getYesod)
 	( return fallback
-	, liftAssistant $ liftAnnex a
+	, liftAssistant $ Assistant.liftAnnex a
 	)
 
+liftAnnex :: forall sub a. Annex a -> GHandler sub WebApp a
+liftAnnex = liftAnnexOr $ error "internal runAnnex"
+
+liftAssistant :: forall sub a. (Assistant a) -> GHandler sub WebApp a
+liftAssistant a = liftIO . flip runAssistant a =<< assistantData <$> getYesod
+
 waitNotifier :: forall sub. (Assistant NotificationBroadcaster) -> NotificationId -> GHandler sub WebApp ()
 waitNotifier getbroadcaster nid = liftAssistant $ do
 	b <- getbroadcaster
@@ -89,12 +93,12 @@
 	r <- readMVar urlrenderer
 	return $ r route params
 
-{- Redirects back to the referring page, or if there's none, HomeR -}
+{- Redirects back to the referring page, or if there's none, DashboardR -}
 redirectBack :: Handler ()
 redirectBack = do
 	clearUltDest
 	setUltDestReferer
-	redirectUltDest HomeR
+	redirectUltDest DashboardR
 
 controlMenu :: Widget
 controlMenu = $(widgetFile "controlmenu")
diff --git a/Assistant/WebApp/Common.hs b/Assistant/WebApp/Common.hs
--- a/Assistant/WebApp/Common.hs
+++ b/Assistant/WebApp/Common.hs
@@ -7,7 +7,7 @@
 
 module Assistant.WebApp.Common (module X) where
 
-import Assistant.Common as X
+import Assistant.Common as X hiding (liftAnnex)
 import Assistant.WebApp as X
 import Assistant.WebApp.Page as X
 import Assistant.WebApp.Form as X
diff --git a/Assistant/WebApp/Configurators.hs b/Assistant/WebApp/Configurators.hs
--- a/Assistant/WebApp/Configurators.hs
+++ b/Assistant/WebApp/Configurators.hs
@@ -10,193 +10,20 @@
 module Assistant.WebApp.Configurators where
 
 import Assistant.WebApp.Common
-import Assistant.DaemonStatus
-import Assistant.WebApp.Notifications
-import Assistant.WebApp.Utility
 import Assistant.WebApp.Configurators.Local
-import qualified Annex
-import qualified Remote
-import qualified Types.Remote as Remote
-import Annex.UUID (getUUID)
-import Logs.Remote
-import Logs.Trust
-import qualified Git
 #ifdef WITH_XMPP
 import Assistant.XMPP.Client
 #endif
 
-import qualified Data.Map as M
-
 {- The main configuration screen. -}
 getConfigurationR :: Handler RepHtml
 getConfigurationR = ifM (inFirstRun)
 	( getFirstRepositoryR
 	, page "Configuration" (Just Configuration) $ do
 #ifdef WITH_XMPP
-		xmppconfigured <- lift $ runAnnex False $ isJust <$> getXMPPCreds
+		xmppconfigured <- lift $ liftAnnex $ isJust <$> getXMPPCreds
 #else
 		let xmppconfigured = False
 #endif
 		$(widgetFile "configurators/main")
 	)
-
-{- An intro message, list of repositories, and nudge to make more. -}
-introDisplay :: Text -> Widget
-introDisplay ident = do
-	webapp <- lift getYesod
-	repolist <- lift $ repoList $ RepoSelector
-		{ onlyCloud = False
-		, onlyConfigured = True
-		, includeHere = False
-		}
-	let n = length repolist
-	let numrepos = show n
-	$(widgetFile "configurators/intro")
-	lift $ modifyWebAppState $ \s -> s { showIntro = False }
-
-makeMiscRepositories :: Widget
-makeMiscRepositories = $(widgetFile "configurators/repositories/misc")
-
-makeCloudRepositories :: Widget
-makeCloudRepositories = $(widgetFile "configurators/repositories/cloud")
-
-{- Lists known repositories, followed by options to add more. -}
-getRepositoriesR :: Handler RepHtml
-getRepositoriesR = page "Repositories" (Just Configuration) $ do
-	let repolist = repoListDisplay $ RepoSelector
-		{ onlyCloud = False
-		, onlyConfigured = False
-		, includeHere = True
-		}
-	$(widgetFile "configurators/repositories")
-
-data Actions
-	= DisabledRepoActions
-		{ setupRepoLink :: Route WebApp }
-	| SyncingRepoActions
-		{ setupRepoLink :: Route WebApp
-		, syncToggleLink :: Route WebApp
-		}
-	| NotSyncingRepoActions
-		{ setupRepoLink :: Route WebApp
-		, syncToggleLink :: Route WebApp
-		}
-
-mkSyncingRepoActions :: UUID -> Actions
-mkSyncingRepoActions u = SyncingRepoActions
-	{ setupRepoLink = EditRepositoryR u
-	, syncToggleLink = DisableSyncR u
-	}
-
-mkNotSyncingRepoActions :: UUID -> Actions
-mkNotSyncingRepoActions u = NotSyncingRepoActions
-	{ setupRepoLink = EditRepositoryR u
-	, syncToggleLink = EnableSyncR u
-	}
-
-needsEnabled :: Actions -> Bool
-needsEnabled (DisabledRepoActions _) = True
-needsEnabled _ = False
-
-notSyncing :: Actions -> Bool
-notSyncing (SyncingRepoActions _ _) = False
-notSyncing _ = True
-
-{- Called by client to get a list of repos, that refreshes
- - when new repos as added.
- -
- - Returns a div, which will be inserted into the calling page.
- -}
-getRepoListR :: RepoListNotificationId -> Handler RepHtml
-getRepoListR (RepoListNotificationId nid reposelector) = do
-	waitNotifier getRepoListBroadcaster nid
-	p <- widgetToPageContent $ repoListDisplay reposelector
-	hamletToRepHtml $ [hamlet|^{pageBody p}|]
-
-repoListDisplay :: RepoSelector -> Widget
-repoListDisplay reposelector = do
-	autoUpdate ident (NotifierRepoListR reposelector) (10 :: Int) (10 :: Int)
-
-	repolist <- lift $ repoList reposelector
-
-	$(widgetFile "configurators/repositories/list")
-
-  where
-	ident = "repolist"
-
-type RepoList = [(String, String, Actions)]
-
-{- A numbered list of known repositories,
- - with actions that can be taken on them. -}
-repoList :: RepoSelector -> Handler RepoList
-repoList reposelector
-	| onlyConfigured reposelector = list =<< configured
-	| otherwise = list =<< (++) <$> configured <*> rest
-  where
-	configured = do
-		rs <- filter wantedrepo . syncRemotes
-			<$> liftAssistant getDaemonStatus
-		runAnnex [] $ do
-			let us = map Remote.uuid rs
-			let l = zip us $ map mkSyncingRepoActions us
-			if includeHere reposelector
-				then do
-					u <- getUUID
-					autocommit <- annexAutoCommit <$> Annex.getGitConfig
-					let hereactions = if autocommit
-						then mkSyncingRepoActions u
-						else mkNotSyncingRepoActions u
-					let here = (u, hereactions)
-					return $ here : l
-				else return l
-	rest = runAnnex [] $ do
-		m <- readRemoteLog
-		unconfigured <- map snd . catMaybes . filter wantedremote 
-			. map (findinfo m)
-			<$> (trustExclude DeadTrusted $ M.keys m)
-		unsyncable <- map Remote.uuid . filter wantedrepo .
-			filter (not . remoteAnnexSync . Remote.gitconfig)
-			<$> Remote.enabledRemoteList
-		return $ zip unsyncable (map mkNotSyncingRepoActions unsyncable) ++ unconfigured
-	wantedrepo r
-		| Remote.readonly r = False
-		| onlyCloud reposelector = Git.repoIsUrl (Remote.repo r) && not (isXMPPRemote r)
-		| otherwise = True
-	wantedremote Nothing = False
-	wantedremote (Just (iscloud, _))
-		| onlyCloud reposelector = iscloud
-		| otherwise = True
-	findinfo m u = case M.lookup u m of
-		Nothing -> Nothing
-		Just c -> case M.lookup "type" c of
-			Just "rsync" -> val True EnableRsyncR
-			Just "directory" -> val False EnableDirectoryR
-#ifdef WITH_S3
-			Just "S3" -> val True EnableS3R
-#endif
-			Just "glacier" -> val True EnableGlacierR
-#ifdef WITH_WEBDAV
-			Just "webdav" -> val True EnableWebDAVR
-#endif
-			_ -> Nothing
-	  where
-		val iscloud r = Just (iscloud, (u, DisabledRepoActions $ r u))
-	list l = runAnnex [] $ do
-		let l' = nubBy (\x y -> fst x == fst y) l
-		zip3
-			<$> pure counter
-			<*> Remote.prettyListUUIDs (map fst l')
-			<*> pure (map snd l')
-	counter = map show ([1..] :: [Int])
-
-getEnableSyncR :: UUID -> Handler ()
-getEnableSyncR = flipSync True
-
-getDisableSyncR :: UUID -> Handler ()
-getDisableSyncR = flipSync False
-
-flipSync :: Bool -> UUID -> Handler ()
-flipSync enable uuid = do
-	mremote <- runAnnex undefined $ Remote.remoteFromUUID uuid
-	changeSyncable mremote enable
-	redirect RepositoriesR
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
@@ -123,7 +123,7 @@
 				]
 		_ -> $(widgetFile "configurators/adds3")
   where
-	setgroup r = runAnnex () $
+	setgroup r = liftAnnex $
 		setStandardGroup (Remote.uuid r) TransferGroup
 #else
 getAddS3R = error "S3 not supported by this build"
@@ -143,7 +143,7 @@
 				]
 		_ -> $(widgetFile "configurators/addglacier")
   where
-	setgroup r = runAnnex () $
+	setgroup r = liftAnnex $
 		setStandardGroup (Remote.uuid r) SmallArchiveGroup
 
 getEnableS3R :: UUID -> Handler RepHtml
@@ -162,20 +162,20 @@
 		runFormGet $ renderBootstrap awsCredsAForm
 	case result of
 		FormSuccess creds -> lift $ do
-			m <- runAnnex M.empty readRemoteLog
+			m <- liftAnnex readRemoteLog
 			let name = fromJust $ M.lookup "name" $
 				fromJust $ M.lookup uuid m
 			makeAWSRemote remotetype creds name (const noop) M.empty
 		_ -> do
-			description <- lift $ runAnnex "" $
+			description <- lift $ liftAnnex $
 				T.pack . concat <$> Remote.prettyListUUIDs [uuid]
 			$(widgetFile "configurators/enableaws")
 
 makeAWSRemote :: RemoteType -> AWSCreds -> String -> (Remote -> Handler ()) -> RemoteConfig -> Handler ()
 makeAWSRemote remotetype (AWSCreds ak sk) name setup config = do
-	remotename <- runAnnex name $ fromRepo $ uniqueRemoteName name 0
+	remotename <- liftAnnex $ fromRepo $ uniqueRemoteName name 0
 	liftIO $ AWS.setCredsEnv (T.unpack ak, T.unpack sk)
-	r <- liftAssistant $ liftAnnex $ addRemote $ do
+	r <- liftAnnex $ addRemote $ do
 		makeSpecialRemote hostname remotetype config
 		return remotename
 	setup r
diff --git a/Assistant/WebApp/Configurators/Edit.hs b/Assistant/WebApp/Configurators/Edit.hs
--- a/Assistant/WebApp/Configurators/Edit.hs
+++ b/Assistant/WebApp/Configurators/Edit.hs
@@ -24,6 +24,7 @@
 import qualified Git
 import qualified Git.Command
 import qualified Git.Config
+import qualified Annex
 import Git.Remote
 
 import qualified Data.Text as T
@@ -46,27 +47,30 @@
 	<$> pure (T.pack $ maybe "here" Remote.name mremote)
 	<*> (maybe Nothing (Just . T.pack) . M.lookup uuid <$> uuidMap)
 	<*> getrepogroup
-	<*> pure (maybe True (remoteAnnexSync . Remote.gitconfig) mremote)
+	<*> getsyncing
   where
 	getrepogroup = do
 		groups <- lookupGroups uuid
 		return $ 
 			maybe (RepoGroupCustom $ unwords $ S.toList groups) RepoGroupStandard
 				(getStandardGroup groups)
+	getsyncing = case mremote of
+		Just r -> return $ remoteAnnexSync $ Remote.gitconfig r
+		Nothing -> annexAutoCommit <$> Annex.getGitConfig
 
 setRepoConfig :: UUID -> Maybe Remote -> RepoConfig -> RepoConfig -> Handler ()
 setRepoConfig uuid mremote oldc newc = do
-	when (repoDescription oldc /= repoDescription newc) $ runAnnex undefined $ do
+	when (repoDescription oldc /= repoDescription newc) $ liftAnnex $ do
 		maybe noop (describeUUID uuid . T.unpack) (repoDescription newc)
 		void uuidMapLoad
-	when (repoGroup oldc /= repoGroup newc) $ runAnnex undefined $ 
+	when (repoGroup oldc /= repoGroup newc) $ liftAnnex $ 
 		case repoGroup newc of
 			RepoGroupStandard g -> setStandardGroup uuid g
 			RepoGroupCustom s -> groupSet uuid $ S.fromList $ words s
 	when (repoSyncable oldc /= repoSyncable newc) $
 		changeSyncable mremote (repoSyncable newc)
 	when (isJust mremote && makeLegalName (T.unpack $ repoName oldc) /= makeLegalName (T.unpack $ repoName newc)) $ do
-		runAnnex undefined $ do
+		liftAnnex $ do
 			name <- fromRepo $ uniqueRemoteName (T.unpack $ repoName newc) 0
 			{- git remote rename expects there to be a
 			 - remote.<name>.fetch, and exits nonzero if
@@ -76,10 +80,11 @@
 			let remotefetch = "remote." ++ T.unpack (repoName oldc) ++ ".fetch"
 			needfetch <- isNothing <$> fromRepo (Git.Config.getMaybe remotefetch)
 			when needfetch $
-				inRepo $ Git.Command.run "config"
-					[Param remotefetch, Param ""]
-			inRepo $ Git.Command.run "remote"
-				[ Param "rename"
+				inRepo $ Git.Command.run
+					[Param "config", Param remotefetch, Param ""]
+			inRepo $ Git.Command.run
+				[ Param "remote"
+				, Param "rename"
 				, Param $ T.unpack $ repoName oldc
 				, Param name
 				]
@@ -114,8 +119,8 @@
 
 editForm :: Bool -> UUID -> Handler RepHtml
 editForm new uuid = page "Configure repository" (Just Configuration) $ do
-	mremote <- lift $ runAnnex undefined $ Remote.remoteFromUUID uuid
-	curr <- lift $ runAnnex undefined $ getRepoConfig uuid mremote
+	mremote <- lift $ liftAnnex $ Remote.remoteFromUUID uuid
+	curr <- lift $ liftAnnex $ getRepoConfig uuid mremote
 	lift $ checkarchivedirectory curr
 	((result, form), enctype) <- lift $
 		runFormGet $ renderBootstrap $ editRepositoryAForm curr
@@ -140,6 +145,6 @@
 		| repoGroup cfg == RepoGroupStandard FullArchiveGroup = go
 		| otherwise = noop
 	  where
-		go = runAnnex undefined $ inRepo $ \g ->
+		go = liftAnnex $ inRepo $ \g ->
 			createDirectoryIfMissing True $
 				Git.repoPath g </> "archive"
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
@@ -7,12 +7,6 @@
 
 {-# LANGUAGE CPP, TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes #-}
 
-#if defined VERSION_yesod_form
-#if ! MIN_VERSION_yesod_form(1,2,0)
-#define WITH_OLD_YESOD
-#endif
-#endif
-
 module Assistant.WebApp.Configurators.Local where
 
 import Assistant.WebApp.Common
@@ -27,7 +21,9 @@
 import qualified Annex
 import Locations.UserConfig
 import Utility.FreeDesktop
+#ifdef WITH_CLIBS
 import Utility.Mounts
+#endif
 import Utility.DiskFree
 import Utility.DataUnits
 import Utility.Network
@@ -41,7 +37,6 @@
 import qualified Data.Text as T
 import Data.Char
 import System.Posix.Directory
-import qualified Control.Exception as E
 
 data RepositoryPath = RepositoryPath Text
 	deriving Show
@@ -52,15 +47,13 @@
  - to use as a repository. -}
 repositoryPathField :: forall sub. Bool -> Field sub WebApp Text
 repositoryPathField autofocus = Field
-#ifdef WITH_OLD_YESOD
+#if ! MIN_VERSION_yesod_form(1,2,0)
 	{ fieldParse = parse
 #else
 	{ fieldParse = \l _ -> parse l
-#endif
-	, fieldView = view
-#ifndef WITH_OLD_YESOD
 	, fieldEnctype = UrlEncoded
 #endif
+	, fieldView = view
 	}
   where
 	view idAttr nameAttr attrs val isReq =
@@ -151,10 +144,10 @@
 	case res of
 		FormSuccess (RepositoryPath p) -> do
 			let path = T.unpack p
-			liftIO $ makeRepo path False
-			u <- liftIO $ initRepo True path Nothing
-			lift $ runAnnex () $ setStandardGroup u ClientGroup
-			liftIO $ addAutoStart path
+			isnew <- liftIO $ makeRepo path False
+			u <- liftIO $ initRepo isnew True path Nothing
+			lift $ liftAnnexOr () $ setStandardGroup u ClientGroup
+			liftIO $ addAutoStartFile path
 			liftIO $ startAssistant path
 			askcombine u path
 		_ -> $(widgetFile "configurators/newrepository")
@@ -164,8 +157,8 @@
 		mainrepo <- fromJust . relDir <$> lift getYesod
 		$(widgetFile "configurators/newrepository/combine")
 
-getCombineRepositoryR :: FilePath -> UUID -> Handler RepHtml
-getCombineRepositoryR newrepopath newrepouuid = do
+getCombineRepositoryR :: FilePathAndUUID -> Handler RepHtml
+getCombineRepositoryR (FilePathAndUUID newrepopath newrepouuid) = do
 	r <- combineRepos newrepopath remotename
 	syncRemote r
 	redirect $ EditRepositoryR newrepouuid
@@ -194,7 +187,11 @@
 				, "free)"
 				]
 
-{- Adding a removable drive. -}
+{- Adding a removable drive.
+ -
+ - The repo may already exist, when adding removable media
+ - that has already been used elsewhere.
+ -}
 getAddDriveR :: Handler RepHtml
 getAddDriveR = page "Add a removable drive" (Just Configuration) $ do
 	removabledrives <- liftIO $ driveList
@@ -208,29 +205,21 @@
 		_ -> $(widgetFile "configurators/adddrive")
   where
 	make mountpoint = do
-		liftIO $ makerepo dir
-		u <- liftIO $ initRepo False dir $ Just remotename
+		liftIO $ createDirectoryIfMissing True dir
+		isnew <- liftIO $ makeRepo dir True
+		u <- liftIO $ initRepo isnew False dir $ Just remotename
 		r <- combineRepos dir remotename
-		runAnnex () $ setStandardGroup u TransferGroup
+		liftAnnex $ setStandardGroup u TransferGroup
 		syncRemote r
 		return u
 	  where
 		dir = mountpoint </> gitAnnexAssistantDefaultDir
 		remotename = takeFileName mountpoint
-	{- The repo may already exist, when adding removable media
-	 - that has already been used elsewhere. -}
-	makerepo dir = liftIO $ do
-		r <- E.try (inDir dir $ getUUID) :: IO (Either E.SomeException UUID)
-		case r of
-			Right u | u /= NoUUID -> noop
-			_ -> do
-				createDirectoryIfMissing True dir
-				makeRepo dir True
 
 {- Each repository is made a remote of the other.
  - Next call syncRemote to get them in sync. -}
 combineRepos :: FilePath -> String -> Handler Remote
-combineRepos dir name = runAnnex undefined $ do
+combineRepos dir name = liftAnnex $ do
 	hostname <- maybe "host" id <$> liftIO getHostname
 	hostlocation <- fromRepo Git.repoLocation
 	liftIO $ inDir dir $ void $ makeGitRemote hostname hostlocation
@@ -238,12 +227,13 @@
 
 getEnableDirectoryR :: UUID -> Handler RepHtml
 getEnableDirectoryR uuid = page "Enable a repository" (Just Configuration) $ do
-	description <- lift $ runAnnex "" $
+	description <- lift $ liftAnnex $
 		T.pack . concat <$> prettyListUUIDs [uuid]
 	$(widgetFile "configurators/enabledirectory")
 
 {- List of removable drives. -}
 driveList :: IO [RemovableDrive]
+#ifdef WITH_CLIBS
 driveList = mapM (gen . mnt_dir) =<< filter sane <$> getMounts
   where
 	gen dir = RemovableDrive
@@ -262,6 +252,9 @@
 		| dir == "/run/shm" = False
 		| dir == "/run/lock" = False
 		| otherwise = True
+#else
+driveList = return []
+#endif
 
 {- Bootstraps from first run mode to a fully running assistant in a
  - repository, by running the postFirstRun callback, which returns the
@@ -270,22 +263,30 @@
 startFullAssistant path = do
 	webapp <- getYesod
 	url <- liftIO $ do
-		makeRepo path False
-		u <- initRepo True path Nothing
+		isnew <- makeRepo path False
+		u <- initRepo isnew True path Nothing
 		inDir path $ 
 			setStandardGroup u ClientGroup
-		addAutoStart path
+		addAutoStartFile path
 		changeWorkingDirectory path
 		fromJust $ postFirstRun webapp
 	redirect $ T.pack url
 
-{- Makes a new git repository. -}
-makeRepo :: FilePath -> Bool -> IO ()
-makeRepo path bare = do
-	(transcript, ok) <- processTranscript "git" (toCommand params) Nothing
-	unless ok $
-		error $ "git init failed!\nOutput:\n" ++ transcript
+{- Makes a new git repository. Or, if a git repository already
+ - exists, returns False. -}
+makeRepo :: FilePath -> Bool -> IO Bool
+makeRepo path bare = ifM alreadyexists
+	( return False
+	, do
+		(transcript, ok) <-
+			processTranscript "git" (toCommand params) Nothing
+		unless ok $
+			error $ "git init failed!\nOutput:\n" ++ transcript
+		return True
+	)
   where
+  	alreadyexists = isJust <$> 
+		catchDefaultIO Nothing (Git.Construct.checkForRepo path)
 	baseparams = [Param "init", Param "--quiet"]
 	params
 		| bare = baseparams ++ [Param "--bare", File path]
@@ -297,30 +298,37 @@
 	state <- Annex.new =<< Git.Config.read =<< Git.Construct.fromPath dir
 	Annex.eval state a
 
-initRepo :: Bool -> FilePath -> Maybe String -> IO UUID
-initRepo primary_assistant_repo dir desc = inDir dir $ do
+{- Creates a new repository, and returns its UUID. -}
+initRepo :: Bool -> Bool -> FilePath -> Maybe String -> IO UUID
+initRepo True primary_assistant_repo dir desc = inDir dir $ do
 	{- Initialize a git-annex repository in a directory with a description. -}
 	unlessM isInitialized $
 		initialize desc
+	{- Initialize the master branch, so things that expect
+	 - to have it will work, before any files are added. -}
 	unlessM (Git.Config.isBare <$> gitRepo) $
-		{- Initialize the master branch, so things that expect
-		 - to have it will work, before any files are added. -}
-		void $ inRepo $ Git.Command.runBool "commit"
-			[ Param "--quiet"
+		void $ inRepo $ Git.Command.runBool
+			[ Param "commit"
+			, Param "--quiet"
 			, Param "--allow-empty"
 			, Param "-m"
 			, Param "created repository"
 			]
-	when primary_assistant_repo $
+	{- Repositories directly managed by the assistant use direct mode.
+	 - 
+	 - Automatic gc is disabled, as it can be slow. Insted, gc is done
+	 - once a day.
+	 -}
+	when primary_assistant_repo $ do
 		setDirect True
+		inRepo $ Git.Command.run
+			[Param "config", Param "gc.auto", Param "0"]
 	getUUID
-
-{- Adds a directory to the autostart file. -}
-addAutoStart :: FilePath -> IO ()
-addAutoStart path = do
-	autostart <- autoStartFile
-	createDirectoryIfMissing True (parentDir autostart)
-	appendFile autostart $ path ++ "\n"
+{- Repo already exists, could be a non-git-annex repo though. -}
+initRepo False _ dir desc = inDir dir $ do
+	unlessM isInitialized $
+		initialize desc
+	getUUID
 
 {- Checks if the user can write to a directory.
  -
diff --git a/Assistant/WebApp/Configurators/Pairing.hs b/Assistant/WebApp/Configurators/Pairing.hs
--- a/Assistant/WebApp/Configurators/Pairing.hs
+++ b/Assistant/WebApp/Configurators/Pairing.hs
@@ -31,7 +31,7 @@
 import Network.Protocol.XMPP
 import Assistant.Types.NetMessager
 import Assistant.NetMessager
-import Assistant.WebApp.Configurators
+import Assistant.WebApp.RepoList
 import Assistant.WebApp.Configurators.XMPP
 #endif
 import Utility.UserInfo
@@ -51,14 +51,14 @@
 
 getStartXMPPPairR :: Handler RepHtml
 #ifdef WITH_XMPP
-getStartXMPPPairR = ifM (isJust <$> runAnnex Nothing getXMPPCreds)
+getStartXMPPPairR = ifM (isJust <$> liftAnnex getXMPPCreds)
 	( do
 		{- Ask buddies to send presence info, to get
 		 - the buddy list populated. -}
 		liftAssistant $ sendNetMessage QueryPresence
 		pairPage $
 			$(widgetFile "configurators/pairing/xmpp/prompt")
-	, redirect XMPPForPairingR -- go get XMPP configured, then come back
+	, redirect XMPPR -- go get XMPP configured, then come back
 	)
 #else
 getStartXMPPPairR = noXMPPPairing
@@ -76,13 +76,12 @@
 	go $ S.toList . buddyAssistants <$> buddy
   where
 	go (Just (clients@((Client exemplar):_))) = do
-		creds <- runAnnex Nothing getXMPPCreds
+		creds <- liftAnnex getXMPPCreds
 		let ourjid = fromJust $ parseJID =<< xmppJID <$> creds
 		let samejid = baseJID ourjid == baseJID exemplar
-		liftAssistant $ do
-			u <- liftAnnex getUUID
-			forM_ clients $ \(Client c) -> sendNetMessage $
-				PairingNotification PairReq (formatJID c) u
+		u <- liftAnnex getUUID
+		liftAssistant $ forM_ clients $ \(Client c) -> sendNetMessage $
+			PairingNotification PairReq (formatJID c) u
 		xmppPairEnd True $ if samejid then Nothing else Just exemplar
 	-- A buddy could have logged out, or the XMPP client restarted,
 	-- and there be no clients to message; handle unforseen by going back.
@@ -109,7 +108,7 @@
 getFinishLocalPairR :: PairMsg -> Handler RepHtml
 #ifdef WITH_PAIRING
 getFinishLocalPairR msg = promptSecret (Just msg) $ \_ secret -> do
-	repodir <- lift $ repoPath <$> runAnnex undefined gitRepo
+	repodir <- lift $ repoPath <$> liftAnnex gitRepo
 	liftIO $ setup repodir
 	startLocalPairing PairAck (cleanup repodir) alert uuid "" secret
   where
@@ -138,8 +137,8 @@
 getFinishXMPPPairR (PairKey theiruuid t) = case parseJID t of
 	Nothing -> error "bad JID"
 	Just theirjid -> do
+		selfuuid <- liftAnnex getUUID
 		liftAssistant $ do
-			selfuuid <- liftAnnex getUUID
 			sendNetMessage $
 				PairingNotification PairAck (formatJID theirjid) selfuuid
 			finishXMPPPairing theirjid theiruuid
@@ -206,7 +205,7 @@
 	{- Sends pairing messages until the thread is killed,
 	 - and shows an activity alert while doing it.
 	 -
-	 - The cancel button returns the user to the HomeR. This is
+	 - The cancel button returns the user to the DashboardR. This is
 	 - not ideal, but they have to be sent somewhere, and could
 	 - have been on a page specific to the in-process pairing
 	 - that just stopped, so can't go back there.
@@ -215,7 +214,7 @@
 		tid <- liftIO myThreadId
 		let selfdestruct = AlertButton
 			{ buttonLabel = "Cancel"
-			, buttonUrl = urlrender HomeR
+			, buttonUrl = urlrender DashboardR
 			, buttonAction = Just $ const $ do
 				oncancel
 				killThread tid
@@ -265,7 +264,7 @@
 secretProblem :: Secret -> Maybe Text
 secretProblem s
 	| B.null s = Just "The secret phrase cannot be left empty. (Remember that punctuation and white space is ignored.)"
-	| B.length s < 7 = Just "Enter a longer secret phrase, at least 6 characters, but really, a phrase is best! This is not a password you'll need to enter every day."
+	| B.length s < 6 = Just "Enter a longer secret phrase, at least 6 characters, but really, a phrase is best! This is not a password you'll need to enter every day."
 	| s == toSecret sampleQuote = Just "Speaking of foolishness, don't paste in the example I gave. Enter a different phrase, please!"
 	| otherwise = Nothing
 
diff --git a/Assistant/WebApp/Configurators/Preferences.hs b/Assistant/WebApp/Configurators/Preferences.hs
new file mode 100644
--- /dev/null
+++ b/Assistant/WebApp/Configurators/Preferences.hs
@@ -0,0 +1,98 @@
+{- git-annex assistant general preferences
+ -
+ - Copyright 2013 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+{-# LANGUAGE QuasiQuotes, TemplateHaskell, OverloadedStrings #-}
+
+module Assistant.WebApp.Configurators.Preferences (
+	getPreferencesR
+) where
+
+import Assistant.WebApp.Common
+import qualified Annex
+import qualified Git
+import Config
+import Locations.UserConfig
+import Utility.DataUnits
+
+import qualified Data.Text as T
+import System.Log.Logger
+
+data PrefsForm = PrefsForm
+	{ diskReserve :: Text
+	, numCopies :: Int
+	, autoStart :: Bool
+	, debugEnabled :: Bool
+	}
+
+prefsAForm :: PrefsForm -> AForm WebApp WebApp PrefsForm
+prefsAForm def = PrefsForm
+	<$> areq (storageField `withNote` diskreservenote)
+		"Disk reserve" (Just $ diskReserve def)
+	<*> areq (positiveIntField `withNote` numcopiesnote)
+		"Number of copies" (Just $ numCopies def)
+	<*> areq (checkBoxField `withNote` autostartnote)
+		"Auto start" (Just $ autoStart def)
+	<*> areq (checkBoxField `withNote` debugnote)
+		"Enable debug logging" (Just $ debugEnabled def)
+  where
+	diskreservenote = [whamlet|<br>Avoid downloading files from other repositories when there is too little free disk space.|]
+	numcopiesnote = [whamlet|<br>Only drop a file after verifying that other repositories contain this many copies.|]
+	debugnote = [whamlet|<a href="@{LogR}">View Log</a>|]
+	autostartnote = [whamlet|Start the git-annex assistant at boot or on login.|]
+
+	positiveIntField = check isPositive intField
+	  where
+		isPositive i
+			| i > 0 = Right i
+			| otherwise = Left notPositive
+		notPositive :: Text
+		notPositive = "This should be 1 or more!"
+
+	storageField = check validStorage textField
+	  where
+		validStorage t
+			| T.null t = Right t
+			| otherwise =  case readSize dataUnits $ T.unpack t of
+				Nothing -> Left badParse
+				Just _ -> Right t
+		badParse :: Text
+		badParse = "Parse error. Expected something like \"100 megabytes\" or \"2 gb\""
+
+getPrefs :: Annex PrefsForm
+getPrefs = PrefsForm
+	<$> (T.pack . roughSize storageUnits False . annexDiskReserve <$> Annex.getGitConfig)
+	<*> (annexNumCopies <$> Annex.getGitConfig)
+	<*> inAutoStartFile
+	<*> ((==) <$> (pure $ Just DEBUG) <*> (liftIO $ getLevel <$> getRootLogger))
+
+storePrefs :: PrefsForm -> Annex ()
+storePrefs p = do
+	setConfig (annexConfig "diskreserve") (T.unpack $ diskReserve p)
+	setConfig (annexConfig "numcopies") (show $ numCopies p)
+	unlessM ((==) <$> pure (autoStart p) <*> inAutoStartFile) $ do
+		here <- fromRepo Git.repoPath
+		liftIO $ if autoStart p
+			then addAutoStartFile here
+			else removeAutoStartFile here
+	liftIO $ updateGlobalLogger rootLoggerName $ setLevel $
+		if debugEnabled p then DEBUG else WARNING
+
+getPreferencesR :: Handler RepHtml
+getPreferencesR = page "Preferences" (Just Configuration) $ do
+	((result, form), enctype) <- lift $ do
+		current <- liftAnnex getPrefs
+		runFormGet $ renderBootstrap $ prefsAForm current
+	case result of
+		FormSuccess new -> lift $ do
+			liftAnnex $ storePrefs new
+			redirect ConfigurationR
+		_ -> $(widgetFile "configurators/preferences")
+
+inAutoStartFile :: Annex Bool
+inAutoStartFile = do
+	here <- fromRepo Git.repoPath
+	any (`equalFilePath` here) <$> liftIO readAutoStartFile
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
@@ -114,7 +114,7 @@
  -}
 getEnableRsyncR :: UUID -> Handler RepHtml
 getEnableRsyncR u = do
-	m <- fromMaybe M.empty . M.lookup u <$> runAnnex M.empty readRemoteLog
+	m <- fromMaybe M.empty . M.lookup u <$> liftAnnex readRemoteLog
 	case (parseSshRsyncUrl =<< M.lookup "rsyncurl" m, M.lookup "name" m) of
 		(Just sshinput, Just reponame) -> sshConfigurator $ do
 			((result, form), enctype) <- lift $
@@ -133,7 +133,7 @@
 		_ -> redirect AddSshR
   where
 	showform form enctype status = do
-		description <- lift $ runAnnex "" $
+		description <- lift $ liftAnnex $
 			T.pack . concat <$> prettyListUUIDs [u]
 		$(widgetFile "configurators/ssh/enable")
 	enable sshdata = lift $ redirect $ ConfirmSshR $
@@ -279,7 +279,7 @@
 
 makeSshRepo :: Bool -> (Remote -> Handler ()) -> SshData -> Handler RepHtml
 makeSshRepo forcersync setup sshdata = do
-	r <- liftAssistant $ makeSshRemote forcersync sshdata
+	r <- liftAssistant $ makeSshRemote forcersync sshdata Nothing
 	setup r
 	redirect $ EditNewCloudRepositoryR $ Remote.uuid r
 
@@ -350,4 +350,4 @@
 isRsyncNet (Just host) = ".rsync.net" `T.isSuffixOf` T.toLower host
 
 setupGroup :: Remote -> Handler ()
-setupGroup r = runAnnex () $ setStandardGroup (Remote.uuid r) TransferGroup
+setupGroup r = liftAnnex $ setStandardGroup (Remote.uuid r) TransferGroup
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
@@ -77,7 +77,7 @@
 				]
 		_ -> $(widgetFile "configurators/addbox.com")
   where
-	setgroup r = runAnnex () $
+	setgroup r = liftAnnex $
 		setStandardGroup (Remote.uuid r) TransferGroup
 #else
 getAddBoxComR = error "WebDAV not supported by this build"
@@ -86,11 +86,11 @@
 getEnableWebDAVR :: UUID -> Handler RepHtml
 #ifdef WITH_WEBDAV
 getEnableWebDAVR uuid = do
-	m <- runAnnex M.empty readRemoteLog
+	m <- liftAnnex readRemoteLog
 	let c = fromJust $ M.lookup uuid m
 	let name = fromJust $ M.lookup "name" c
 	let url = fromJust $ M.lookup "url" c
-	mcreds <- runAnnex Nothing $
+	mcreds <- liftAnnex $
 		getRemoteCredPairFor "webdav" c (WebDAV.davCreds uuid)
 	case mcreds of
 		Just creds -> webDAVConfigurator $ lift $
@@ -108,7 +108,7 @@
 			FormSuccess input -> lift $
 				makeWebDavRemote name (toCredPair input) (const noop) M.empty
 			_ -> do
-				description <- lift $ runAnnex "" $
+				description <- lift $ liftAnnex $
 					T.pack . concat <$> Remote.prettyListUUIDs [uuid]
 				$(widgetFile "configurators/enablewebdav")
 #else
@@ -118,9 +118,9 @@
 #ifdef WITH_WEBDAV
 makeWebDavRemote :: String -> CredPair -> (Remote -> Handler ()) -> RemoteConfig -> Handler ()
 makeWebDavRemote name creds setup config = do
-	remotename <- runAnnex name $ fromRepo $ uniqueRemoteName name 0
+	remotename <- liftAnnex $ fromRepo $ uniqueRemoteName name 0
 	liftIO $ WebDAV.setCredsEnv creds
-	r <- liftAssistant $ liftAnnex $ addRemote $ do
+	r <- liftAnnex $ addRemote $ do
 		makeSpecialRemote name WebDAV.remote config
 		return remotename
 	setup r
diff --git a/Assistant/WebApp/Configurators/XMPP.hs b/Assistant/WebApp/Configurators/XMPP.hs
--- a/Assistant/WebApp/Configurators/XMPP.hs
+++ b/Assistant/WebApp/Configurators/XMPP.hs
@@ -33,7 +33,7 @@
 {- Displays an alert suggesting to configure XMPP, with a button. -}
 xmppNeeded :: Handler ()
 #ifdef WITH_XMPP
-xmppNeeded = whenM (isNothing <$> runAnnex Nothing getXMPPCreds) $ do
+xmppNeeded = whenM (isNothing <$> liftAnnex getXMPPCreds) $ do
 	urlrender <- getUrlRender
 	void $ liftAssistant $ do
 		close <- asIO1 removeAlert
@@ -48,25 +48,9 @@
 
 getXMPPR :: Handler RepHtml
 #ifdef WITH_XMPP
-getXMPPR = getXMPPR' ConfigurationR
-#else
-getXMPPR = xmppPage $
-	$(widgetFile "configurators/xmpp/disabled")
-#endif
-
-getXMPPForPairingR :: Handler RepHtml
-#ifdef WITH_XMPP
-getXMPPForPairingR = getXMPPR' StartXMPPPairR
-#else
-getXMPPForPairingR = xmppPage $
-	$(widgetFile "configurators/xmpp/disabled")
-#endif
-
-#ifdef WITH_XMPP
-getXMPPR' :: Route WebApp -> Handler RepHtml
-getXMPPR' redirto = xmppPage $ do
+getXMPPR = xmppPage $ do
 	((result, form), enctype) <- lift $ do
-		oldcreds <- runAnnex Nothing getXMPPCreds
+		oldcreds <- liftAnnex getXMPPCreds
 		runFormGet $ renderBootstrap $ xmppAForm $
 			creds2Form <$> oldcreds
 	let showform problem = $(widgetFile "configurators/xmpp")
@@ -76,9 +60,12 @@
 		_ -> showform Nothing
   where
 	storecreds creds = do
-		void $ runAnnex undefined $ setXMPPCreds creds
+		void $ liftAnnex $ setXMPPCreds creds
 		liftAssistant notifyNetMessagerRestart
-		redirect redirto
+		redirect StartXMPPPairR
+#else
+getXMPPR = xmppPage $
+	$(widgetFile "configurators/xmpp/disabled")
 #endif
 
 {- Called by client to get a list of buddies.
@@ -96,6 +83,7 @@
 buddyListDisplay = do
 	autoUpdate ident NotifierBuddyListR (10 :: Int) (10 :: Int)
 #ifdef WITH_XMPP
+	myjid <- lift $ liftAssistant $ xmppClientID <$> getDaemonStatus
 	buddies <- lift $ liftAssistant $ do
 		rs <- filter isXMPPRemote . syncGitRemotes <$> getDaemonStatus
 		let pairedwith = catMaybes $ map (parseJID . getXMPPClientID) rs
diff --git a/Assistant/WebApp/Control.hs b/Assistant/WebApp/Control.hs
--- a/Assistant/WebApp/Control.hs
+++ b/Assistant/WebApp/Control.hs
@@ -52,7 +52,7 @@
 
 getLogR :: Handler RepHtml
 getLogR = page "Logs" Nothing $ do
-	logfile <- lift $ runAnnex undefined $ fromRepo gitAnnexLogFile
+	logfile <- lift $ liftAnnex $ fromRepo gitAnnexLogFile
 	logs <- liftIO $ listLogs logfile
 	logcontent <- liftIO $ concat <$> mapM readFile logs
 	$(widgetFile "control/log")
diff --git a/Assistant/WebApp/DashBoard.hs b/Assistant/WebApp/DashBoard.hs
--- a/Assistant/WebApp/DashBoard.hs
+++ b/Assistant/WebApp/DashBoard.hs
@@ -12,7 +12,7 @@
 import Assistant.WebApp.Common
 import Assistant.WebApp.Utility
 import Assistant.WebApp.Notifications
-import Assistant.WebApp.Configurators
+import Assistant.WebApp.RepoList
 import Assistant.TransferQueue
 import Utility.NotificationBroadcaster
 import Logs.Transfer
@@ -33,7 +33,7 @@
 transfersDisplay warnNoScript = do
 	webapp <- lift getYesod
 	current <- lift $ M.toList <$> getCurrentTransfers
-	queued <- lift $ liftAssistant getTransferQueue
+	queued <- lift $ take 10 <$> liftAssistant getTransferQueue
 	autoUpdate ident NotifierTransfersR (10 :: Int) (10 :: Int)
 	let transfers = simplifyTransfers $ current ++ queued
 	if null transfers
@@ -62,7 +62,7 @@
  -
  - Note that the head of the widget is not included, only its
  - body is. To get the widget head content, the widget is also 
- - inserted onto the getHomeR page.
+ - inserted onto the getDashboardR page.
  -}
 getTransfersR :: NotificationId -> Handler RepHtml
 getTransfersR nid = do
@@ -77,21 +77,21 @@
 	let content = transfersDisplay warnNoScript
 	$(widgetFile "dashboard/main")
 
-getHomeR :: Handler RepHtml
-getHomeR = ifM (inFirstRun)
+getDashboardR :: Handler RepHtml
+getDashboardR = ifM (inFirstRun)
 	( redirect ConfigurationR
 	, page "" (Just DashBoard) $ dashboard True
 	)
 
 {- Used to test if the webapp is running. -}
-headHomeR :: Handler ()
-headHomeR = noop
+headDashboardR :: Handler ()
+headDashboardR = noop
 
-{- Same as HomeR, except no autorefresh at all (and no noscript warning). -}
+{- Same as DashboardR, except no autorefresh at all (and no noscript warning). -}
 getNoScriptR :: Handler RepHtml
 getNoScriptR = page "" (Just DashBoard) $ dashboard False
 
-{- Same as HomeR, except with autorefreshing via meta refresh. -}
+{- Same as DashboardR, except with autorefreshing via meta refresh. -}
 getNoScriptAutoR :: Handler RepHtml
 getNoScriptAutoR = page "" (Just DashBoard) $ do
 	let ident = NoScriptR
@@ -117,8 +117,7 @@
  - blocking the response to the browser on it. -}
 openFileBrowser :: Handler Bool
 openFileBrowser = do
-	path <- runAnnex (error "no configured repository") $
-		fromRepo Git.repoPath
+	path <- liftAnnex $ fromRepo Git.repoPath
 	ifM (liftIO $ inPath cmd <&&> inPath cmd)
 		( do
 			void $ liftIO $ forkIO $ void $
diff --git a/Assistant/WebApp/Documentation.hs b/Assistant/WebApp/Documentation.hs
--- a/Assistant/WebApp/Documentation.hs
+++ b/Assistant/WebApp/Documentation.hs
@@ -12,6 +12,7 @@
 import Assistant.WebApp.Common
 import Assistant.Install (standaloneAppBase)
 import Build.SysConfig (packageversion)
+import BuildFlags
 
 {- The full license info may be included in a file on disk that can
  - be read in and displayed. -}
diff --git a/Assistant/WebApp/Form.hs b/Assistant/WebApp/Form.hs
--- a/Assistant/WebApp/Form.hs
+++ b/Assistant/WebApp/Form.hs
@@ -5,7 +5,9 @@
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
-{-# LANGUAGE FlexibleContexts, TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes #-}
+{-# LANGUAGE FlexibleContexts, TypeFamilies, QuasiQuotes #-}
+{-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-}
+{-# LANGUAGE OverloadedStrings, RankNTypes #-}
 
 module Assistant.WebApp.Form where
 
@@ -44,19 +46,6 @@
 	newview theId name attrs val isReq = 
 		let fieldwidget = (fieldView field) theId name attrs val isReq
 		in [whamlet|^{fieldwidget}&nbsp;&nbsp;<span>^{note}</span>|]
-
-
-{- Makes a help button be displayed after a field, that displays a help
- - widget when clicked. Requires a unique ident for the help. -}
-withHelp :: Field sub master v -> GWidget sub master () -> Text -> Field sub master v
-withHelp field help ident = withNote field note
-  where
-	note = [whamlet|
-<a .btn data-toggle="collapse" data-target="##{ident}">
-  Help
-<div ##{ident} .collapse>
-  ^{help}
-|]
 
 data EnableEncryption = SharedEncryption | NoEncryption
 	deriving (Eq)
diff --git a/Assistant/WebApp/OtherRepos.hs b/Assistant/WebApp/OtherRepos.hs
--- a/Assistant/WebApp/OtherRepos.hs
+++ b/Assistant/WebApp/OtherRepos.hs
@@ -29,11 +29,10 @@
 
 listOtherRepos :: IO [(String, String)]
 listOtherRepos = do
-	f <- autoStartFile
+	dirs <- readAutoStartFile
 	pwd <- getCurrentDirectory
-	dirs <- filter (\d -> not $ d `dirContains` pwd) . nub
-		<$> ifM (doesFileExist f) ( lines <$> readFile f, return [])
-	gooddirs <- filterM doesDirectoryExist dirs
+	gooddirs <- filterM doesDirectoryExist $
+		filter (\d -> not $ d `dirContains` pwd) dirs
 	names <- mapM relHome gooddirs
 	return $ sort $ zip names gooddirs
 
diff --git a/Assistant/WebApp/Page.hs b/Assistant/WebApp/Page.hs
--- a/Assistant/WebApp/Page.hs
+++ b/Assistant/WebApp/Page.hs
@@ -19,21 +19,23 @@
 import Text.Hamlet
 import Data.Text (Text)
 
-data NavBarItem = DashBoard | Configuration | About
-	deriving (Eq)
+data NavBarItem = DashBoard | Repositories | Configuration | About
+	deriving (Eq, Ord, Enum, Bounded)
 
 navBarName :: NavBarItem -> Text
 navBarName DashBoard = "Dashboard"
+navBarName Repositories = "Repositories"
 navBarName Configuration = "Configuration"
 navBarName About = "About"
 
 navBarRoute :: NavBarItem -> Route WebApp
-navBarRoute DashBoard = HomeR
+navBarRoute DashBoard = DashboardR
+navBarRoute Repositories = RepositoriesR
 navBarRoute Configuration = ConfigurationR
 navBarRoute About = AboutR
 
 defaultNavBar :: [NavBarItem]
-defaultNavBar = [DashBoard, Configuration, About]
+defaultNavBar = [minBound .. maxBound]
 
 firstRunNavBar :: [NavBarItem]
 firstRunNavBar = [Configuration, About]
diff --git a/Assistant/WebApp/RepoList.hs b/Assistant/WebApp/RepoList.hs
new file mode 100644
--- /dev/null
+++ b/Assistant/WebApp/RepoList.hs
@@ -0,0 +1,227 @@
+{- git-annex assistant webapp repository list
+ -
+ - Copyright 2012 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes, CPP #-}
+
+module Assistant.WebApp.RepoList where
+
+import Assistant.WebApp.Common
+import Assistant.DaemonStatus
+import Assistant.WebApp.Notifications
+import Assistant.WebApp.Utility
+import qualified Annex
+import qualified Remote
+import qualified Types.Remote as Remote
+import Remote.List (remoteListRefresh)
+import Annex.UUID (getUUID)
+import Logs.Remote
+import Logs.Trust
+import Config
+import Config.Cost
+import qualified Git
+#ifdef WITH_XMPP
+#endif
+
+import qualified Data.Map as M
+import qualified Data.Set as S
+import qualified Data.Text as T
+
+{- An intro message, list of repositories, and nudge to make more. -}
+introDisplay :: Text -> Widget
+introDisplay ident = do
+	webapp <- lift getYesod
+	repolist <- lift $ repoList $ RepoSelector
+		{ onlyCloud = False
+		, onlyConfigured = True
+		, includeHere = False
+		}
+	let n = length repolist
+	let numrepos = show n
+	$(widgetFile "configurators/intro")
+	lift $ modifyWebAppState $ \s -> s { showIntro = False }
+
+makeMiscRepositories :: Widget
+makeMiscRepositories = $(widgetFile "configurators/repositories/misc")
+
+makeCloudRepositories :: Bool -> Widget
+makeCloudRepositories onlyTransfer = $(widgetFile "configurators/repositories/cloud")
+
+{- Lists known repositories, followed by options to add more. -}
+getRepositoriesR :: Handler RepHtml
+getRepositoriesR = page "Repositories" (Just Repositories) $ do
+	let repolist = repoListDisplay $ RepoSelector
+		{ onlyCloud = False
+		, onlyConfigured = False
+		, includeHere = True
+		}
+	$(widgetFile "configurators/repositories")
+
+data Actions
+	= DisabledRepoActions
+		{ setupRepoLink :: Route WebApp }
+	| SyncingRepoActions
+		{ setupRepoLink :: Route WebApp
+		, syncToggleLink :: Route WebApp
+		}
+	| NotSyncingRepoActions
+		{ setupRepoLink :: Route WebApp
+		, syncToggleLink :: Route WebApp
+		}
+
+mkSyncingRepoActions :: UUID -> Actions
+mkSyncingRepoActions u = SyncingRepoActions
+	{ setupRepoLink = EditRepositoryR u
+	, syncToggleLink = DisableSyncR u
+	}
+
+mkNotSyncingRepoActions :: UUID -> Actions
+mkNotSyncingRepoActions u = NotSyncingRepoActions
+	{ setupRepoLink = EditRepositoryR u
+	, syncToggleLink = EnableSyncR u
+	}
+
+needsEnabled :: Actions -> Bool
+needsEnabled (DisabledRepoActions _) = True
+needsEnabled _ = False
+
+notSyncing :: Actions -> Bool
+notSyncing (SyncingRepoActions _ _) = False
+notSyncing _ = True
+
+{- Called by client to get a list of repos, that refreshes
+ - when new repos as added.
+ -
+ - Returns a div, which will be inserted into the calling page.
+ -}
+getRepoListR :: RepoListNotificationId -> Handler RepHtml
+getRepoListR (RepoListNotificationId nid reposelector) = do
+	waitNotifier getRepoListBroadcaster nid
+	p <- widgetToPageContent $ repoListDisplay reposelector
+	hamletToRepHtml $ [hamlet|^{pageBody p}|]
+
+repoListDisplay :: RepoSelector -> Widget
+repoListDisplay reposelector = do
+	autoUpdate ident (NotifierRepoListR reposelector) (10 :: Int) (10 :: Int)
+	addScript $ StaticR jquery_ui_core_js
+	addScript $ StaticR jquery_ui_widget_js
+	addScript $ StaticR jquery_ui_mouse_js
+	addScript $ StaticR jquery_ui_sortable_js
+
+	repolist <- lift $ repoList reposelector
+
+	$(widgetFile "configurators/repositories/list")
+
+  where
+	ident = "repolist"
+
+-- (num, name, uuid, actions)
+type RepoList = [(String, String, UUID, Actions)]
+
+{- A numbered list of known repositories,
+ - with actions that can be taken on them. -}
+repoList :: RepoSelector -> Handler RepoList
+repoList reposelector
+	| onlyConfigured reposelector = list =<< configured
+	| otherwise = list =<< (++) <$> configured <*> unconfigured
+  where
+	configured = do
+		syncing <- S.fromList . syncRemotes
+			<$> liftAssistant getDaemonStatus
+		liftAnnex $ do
+			rs <- filter wantedrepo . concat . Remote.byCost
+				<$> Remote.enabledRemoteList
+			let us = map Remote.uuid rs
+			let make r = if r `S.member` syncing
+				then mkSyncingRepoActions $ Remote.uuid r
+				else mkNotSyncingRepoActions $ Remote.uuid r
+			let l = zip us $ map make rs
+			if includeHere reposelector
+				then do
+					u <- getUUID
+					autocommit <- annexAutoCommit <$> Annex.getGitConfig
+					let hereactions = if autocommit
+						then mkSyncingRepoActions u
+						else mkNotSyncingRepoActions u
+					let here = (u, hereactions)
+					return $ here : l
+				else return l
+	unconfigured = liftAnnex $ do
+		m <- readRemoteLog
+		map snd . catMaybes . filter wantedremote 
+			. map (findinfo m)
+			<$> (trustExclude DeadTrusted $ M.keys m)
+	wantedrepo r
+		| Remote.readonly r = False
+		| onlyCloud reposelector = Git.repoIsUrl (Remote.repo r) && not (isXMPPRemote r)
+		| otherwise = True
+	wantedremote Nothing = False
+	wantedremote (Just (iscloud, _))
+		| onlyCloud reposelector = iscloud
+		| otherwise = True
+	findinfo m u = case M.lookup u m of
+		Nothing -> Nothing
+		Just c -> case M.lookup "type" c of
+			Just "rsync" -> val True EnableRsyncR
+			Just "directory" -> val False EnableDirectoryR
+#ifdef WITH_S3
+			Just "S3" -> val True EnableS3R
+#endif
+			Just "glacier" -> val True EnableGlacierR
+#ifdef WITH_WEBDAV
+			Just "webdav" -> val True EnableWebDAVR
+#endif
+			_ -> Nothing
+	  where
+		val iscloud r = Just (iscloud, (u, DisabledRepoActions $ r u))
+	list l = liftAnnex $ do
+		let l' = nubBy (\x y -> fst x == fst y) l
+		l'' <- zip3
+			<$> pure counter
+			<*> Remote.prettyListUUIDs (map fst l')
+			<*> pure l'
+		return $ map (\(num, name, (uuid, actions)) -> (num, name, uuid, actions)) l''
+	counter = map show ([1..] :: [Int])
+
+getEnableSyncR :: UUID -> Handler ()
+getEnableSyncR = flipSync True
+
+getDisableSyncR :: UUID -> Handler ()
+getDisableSyncR = flipSync False
+
+flipSync :: Bool -> UUID -> Handler ()
+flipSync enable uuid = do
+	mremote <- liftAnnex $ Remote.remoteFromUUID uuid
+	changeSyncable mremote enable
+	redirect RepositoriesR
+
+getRepositoriesReorderR :: Handler ()
+getRepositoriesReorderR = do
+	{- Get uuid of the moved item, and the list it was moved within. -}
+	moved <- fromjs <$> runInputGet (ireq textField "moved")
+	list <- map fromjs <$> lookupGetParams "list[]"
+	void $ liftAnnex $ do
+		remote <- fromMaybe (error "Unknown UUID") <$>
+			Remote.remoteFromUUID moved
+		rs <- catMaybes <$> mapM Remote.remoteFromUUID list
+		forM_ (reorderCosts remote rs) $ \(r, newcost) ->
+			when (Remote.cost r /= newcost) $
+				setRemoteCost r newcost
+		remoteListRefresh
+	liftAssistant updateSyncRemotes
+  where
+  	fromjs = toUUID . T.unpack
+
+reorderCosts :: Remote -> [Remote] -> [(Remote, Cost)]
+reorderCosts remote rs = zip rs'' (insertCostAfter costs i)
+  where
+	{- Find the index of the remote in the list that the remote
+	 - was moved to be after.
+	 - If it was moved to the start of the list, -1 -}
+	i = fromMaybe 0 (elemIndex remote rs) - 1
+	rs' = filter (\r -> Remote.uuid r /= Remote.uuid remote) rs
+	costs = map Remote.cost rs'
+	rs'' = (\(x, y) -> x ++ [remote] ++ y) $ splitAt (i + 1) rs'
diff --git a/Assistant/WebApp/Types.hs b/Assistant/WebApp/Types.hs
--- a/Assistant/WebApp/Types.hs
+++ b/Assistant/WebApp/Types.hs
@@ -5,7 +5,8 @@
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
-{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes #-}
+{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses #-}
+{-# LANGUAGE TemplateHaskell, OverloadedStrings, RankNTypes #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 
 module Assistant.WebApp.Types where
@@ -87,6 +88,15 @@
 
 data RepoListNotificationId = RepoListNotificationId NotificationId RepoSelector
 	deriving (Read, Show, Eq)
+
+{- Only needed to work around old-yesod bug that emits a warning message
+ - when a route has two parameters. -}
+data FilePathAndUUID = FilePathAndUUID FilePath UUID
+	deriving (Read, Show, Eq)
+
+instance PathPiece FilePathAndUUID where
+	toPathPiece = pack . show
+	fromPathPiece = readish . unpack
 
 instance PathPiece SshData where
 	toPathPiece = pack . show
diff --git a/Assistant/WebApp/Utility.hs b/Assistant/WebApp/Utility.hs
--- a/Assistant/WebApp/Utility.hs
+++ b/Assistant/WebApp/Utility.hs
@@ -7,7 +7,7 @@
 
 module Assistant.WebApp.Utility where
 
-import Assistant.Common
+import Assistant.Common hiding (liftAnnex)
 import Assistant.WebApp
 import Assistant.WebApp.Types
 import Assistant.DaemonStatus
@@ -34,10 +34,10 @@
 {- Use Nothing to change autocommit setting; or a remote to change
  - its sync setting. -}
 changeSyncable :: (Maybe Remote) -> Bool -> Handler ()
-changeSyncable Nothing enable = liftAssistant $ do
+changeSyncable Nothing enable = do
 	liftAnnex $ Config.setConfig key (boolConfig enable)
 	liftIO . maybe noop (`throwTo` signal)
-		=<< namedThreadId watchThread
+		=<< liftAssistant (namedThreadId watchThread)
   where
 	key = Config.annexConfig "autocommit"
 	signal
@@ -59,7 +59,7 @@
 	tofrom t = transferUUID t == Remote.uuid r
 
 changeSyncFlag :: Remote -> Bool -> Handler ()
-changeSyncFlag r enabled = runAnnex undefined $ do
+changeSyncFlag r enabled = liftAnnex $ do
 	Config.setConfig key (boolConfig enabled)
 	void $ Remote.remoteListRefresh
   where
diff --git a/Assistant/WebApp/routes b/Assistant/WebApp/routes
--- a/Assistant/WebApp/routes
+++ b/Assistant/WebApp/routes
@@ -1,6 +1,11 @@
-/ HomeR GET HEAD
+/ DashboardR GET HEAD
+
+/repositories RepositoriesR GET
+/repositories/reorder RepositoriesReorderR GET
+
 /noscript NoScriptR GET
 /noscript/auto NoScriptAutoR GET
+
 /about AboutR GET
 /about/license LicenseR GET
 /about/repogroups RepoGroupR GET
@@ -12,15 +17,14 @@
 /log LogR GET
 
 /config ConfigurationR GET
-/config/repository RepositoriesR GET
+/config/preferences PreferencesR GET
 /config/xmpp XMPPR GET
-/config/xmpp/for/pairing XMPPForPairingR GET
 
 /config/repository/new/first FirstRepositoryR GET
 /config/repository/new NewRepositoryR GET
 /config/repository/switcher RepositorySwitcherR GET
 /config/repository/switchto/#FilePath SwitchToRepositoryR GET
-/config/repository/combine/#FilePath/#UUID CombineRepositoryR GET
+/config/repository/combine/#FilePathAndUUID CombineRepositoryR GET
 /config/repository/edit/#UUID EditRepositoryR GET
 /config/repository/edit/new/#UUID EditNewRepositoryR GET
 /config/repository/edit/new/cloud/#UUID EditNewCloudRepositoryR GET
diff --git a/Assistant/XMPP.hs b/Assistant/XMPP.hs
--- a/Assistant/XMPP.hs
+++ b/Assistant/XMPP.hs
@@ -52,9 +52,8 @@
 	extractGitAnnexTag = headMaybe . filter isGitAnnexTag . messagePayloads
 
 instance GitAnnexTaggable Presence where
-	-- always mark extended away and set presence priority to negative
 	insertGitAnnexTag p elt = p
-		{ presencePayloads = extendedAway : negativePriority : elt : presencePayloads p }
+		{ presencePayloads = negativePriority : elt : presencePayloads p }
 	extractGitAnnexTag = headMaybe . filter isGitAnnexTag . presencePayloads
 
 data GitAnnexTagInfo = GitAnnexTagInfo
@@ -204,10 +203,6 @@
 		, elementAttributes = []
 		, elementNodes = []
 		}
-
-{- Add to a presence to mark its client as extended away. -}
-extendedAway :: Element
-extendedAway = Element "show" [] [NodeContent $ ContentText "xa"]
 
 {- Add to a presence to give it a negative priority. -}
 negativePriority :: Element
diff --git a/Assistant/XMPP/Git.hs b/Assistant/XMPP/Git.hs
--- a/Assistant/XMPP/Git.hs
+++ b/Assistant/XMPP/Git.hs
@@ -19,11 +19,14 @@
 import qualified Command.Sync
 import qualified Annex.Branch
 import Annex.UUID
+import Annex.TaggedPush
 import Config
 import Git
 import qualified Git.Branch
 import Locations.UserConfig
 import qualified Types.Remote as Remote
+import qualified Remote as Remote
+import Remote.List
 import Utility.FileMode
 import Utility.Shell
 
@@ -52,7 +55,10 @@
 	remote <- liftAnnex $ addRemote $
 		makeGitRemote buddyname $ gitXMPPLocation jid
 	liftAnnex $ storeUUID (remoteConfig (Remote.repo remote) "uuid") u
-	syncNewRemote remote
+	liftAnnex $ void remoteListRefresh
+	remote' <- liftAnnex $ fromMaybe (error "failed to add remote")
+		<$> Remote.byName (Just buddyname)
+	syncNewRemote remote'
 	return True
 
 {- Pushes over XMPP, communicating with a specific client.
@@ -244,14 +250,10 @@
   where
 	matching loc r = repoIsUrl r && repoLocation r == loc
 
-whenXMPPRemote :: ClientID -> Assistant () -> Assistant ()
-whenXMPPRemote cid = unlessM (null <$> xmppRemotes cid)
-
 handlePushInitiation :: NetMessage -> Assistant ()
 handlePushInitiation (Pushing cid CanPush) =
-	whenXMPPRemote cid $ 
+	unlessM (null <$> xmppRemotes cid) $ 
 		sendNetMessage $ Pushing cid PushRequest
-
 handlePushInitiation (Pushing cid PushRequest) =
 	go =<< liftAnnex (inRepo Git.Branch.current)
   where
@@ -263,12 +265,14 @@
 			<$> gitRepo
 			<*> getUUID
 		liftIO $ Command.Sync.updateBranch (Command.Sync.syncBranch branch) g
-		debug ["pushing to", show rs]
-		forM_ rs $ \r -> xmppPush cid $ pushFallback u branch r
-
-handlePushInitiation (Pushing cid StartingPush) =
-	whenXMPPRemote cid $
-		void $ xmppReceivePack cid
+		selfjid <- ((T.unpack <$>) . xmppClientID) <$> getDaemonStatus
+		forM_ rs $ \r -> alertWhile (syncAlert [r]) $
+			xmppPush cid $ taggedPush u selfjid branch r
+handlePushInitiation (Pushing cid StartingPush) = do
+	rs <- xmppRemotes cid
+	unless (null rs) $
+		void $ alertWhile (syncAlert rs) $
+			xmppReceivePack cid
 handlePushInitiation _ = noop
 
 handleDeferred :: NetMessage -> Assistant ()
diff --git a/Build/mdwn2man b/Build/mdwn2man
--- a/Build/mdwn2man
+++ b/Build/mdwn2man
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
 # Warning: hack
 
 my $prog=shift;
diff --git a/BuildFlags.hs b/BuildFlags.hs
new file mode 100644
--- /dev/null
+++ b/BuildFlags.hs
@@ -0,0 +1,51 @@
+{- git-annex build flags reporting
+ -
+ - Copyright 2013 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE CPP #-}
+
+module BuildFlags where
+
+buildFlags :: [String]
+buildFlags = filter (not . null)
+	[ ""
+#ifdef WITH_ASSISTANT
+	, "Assistant"
+#endif
+#ifdef WITH_WEBAPP
+	, "Webapp"
+#endif
+#ifdef WITH_PAIRING
+	, "Pairing"
+#endif
+#ifdef WITH_TESTSUITE
+	, "Testsuite"
+#endif
+#ifdef WITH_S3
+	, "S3"
+#endif
+#ifdef WITH_WEBDAV
+	, "WebDAV"
+#endif
+#ifdef WITH_INOTIFY
+	, "Inotify"
+#endif
+#ifdef WITH_FSEVENTS
+	, "FsEvents"
+#endif
+#ifdef WITH_KQUEUE
+	, "Kqueue"
+#endif
+#ifdef WITH_DBUS
+	, "DBus"
+#endif
+#ifdef WITH_XMPP
+	, "XMPP"
+#endif
+#ifdef WITH_DNS
+	, "DNS"
+#endif
+	]
diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,78 @@
+git-annex (4.20130314) unstable; urgency=low
+
+  * Bugfix: git annex add, when ran without any file or directory specified,
+    should add files in the current directory, but not act on unlocked files
+    elsewhere in the tree.
+  * Bugfix: drop --from an unavailable remote no longer updates the location
+    log, incorrectly, to say the remote does not have the key.
+  * Bugfix: If the UUID of a remote is not known, prevent --from, --to,
+    and other ways of specifying remotes by name from selecting it,
+    since it is not possible to sanely use it.
+  * Bugfix: Fix bug in inode cache sentinal check, which broke
+    copying to local repos if the repo being copied from had moved
+    to a different filesystem or otherwise changed all its inodes
+
+  * Switch from using regex-compat to regex-tdfa, as the C regex library
+    is rather buggy.
+  * status: Can now be run with a directory path to show only the
+    status of that directory, rather than the whole annex.
+  * Added remote.<name>.annex-gnupg-options setting.
+    Thanks, guilhem for the patch.
+  * addurl: Add --relaxed option.
+  * addurl: Escape invalid characters in urls, rather than failing to
+    use an invalid url.
+  * addurl: Properly handle url-escaped characters in file:// urls.
+
+  * assistant: Fix dropping content when a file is moved to an archive
+    directory, and getting contennt when a file is moved back out.
+  * assistant: Fix bug in direct mode that could occur when a symlink is
+    moved out of an archive directory, and resulted in the file not being
+    set to direct mode when it was transferred.
+  * assistant: Generate better commits for renames.
+  * assistant: Logs are rotated to avoid them using too much disk space.
+  * assistant: Avoid noise in logs from git commit about typechanged
+    files in direct mode repositories.
+  * assistant: Set gc.auto=0 when creating repositories to prevent
+    automatic commits from causing git-gc runs.
+  * assistant: If gc.auto=0, run git-gc once a day, packing loose objects
+    very non-aggressively.
+  * assistant: XMPP git pull and push requests are cached and sent when
+    presence of a new client is detected.
+  * assistant: Sync with all git remotes on startup.
+  * assistant: Get back in sync with XMPP remotes after network reconnection,
+    and on startup.
+  * assistant: Fix syncing after XMPP pairing.
+  * assistant: Optimised handling of renamed files in direct mode,
+    avoiding re-checksumming.
+  * assistant: Detects most renames, including directory renames, and
+    combines all their changes into a single commit.
+  * assistant: Fix ~/.ssh/git-annex-shell wrapper to work when the
+    ssh key does not force a command.
+  * assistant: Be smarter about avoiding unncessary transfers.
+
+  * webapp: Work around bug in Warp's slowloris attack prevention code,
+    that caused regular browsers to stall when they reuse a connection
+    after leaving it idle for 30 seconds.
+    (See https://github.com/yesodweb/wai/issues/146)
+  * webapp: New preferences page allows enabling/disabling debug logging
+    at runtime, as well as configuring numcopies and diskreserve.
+  * webapp: Repository costs can be configured by dragging repositories around
+    in the repository list.
+  * webapp: Proceed automatically on from "Configure jabber account"
+    to pairing.
+  * webapp: Only show up to 10 queued transfers.
+  * webapp: DTRT when told to create a git repo that already exists.
+  * webapp: Set locally paired repositories to a lower cost than other
+    network remotes.
+
+  * Run ssh with -T to avoid tty allocation and any login scripts that
+    may do undesired things with it.
+  * Several improvements to Makefile and cabal file. Thanks, Peter Simmons
+  * Stop depending on testpack.
+  * Android: Enable test suite. 
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 14 Mar 2013 15:29:20 -0400
+
 git-annex (4.20130227) unstable; urgency=low
 
   * annex.version is now set to 4 for direct mode repositories.
diff --git a/COPYRIGHT b/COPYRIGHT
--- a/COPYRIGHT
+++ b/COPYRIGHT
@@ -6,8 +6,17 @@
 License: GPL-3+
 
 Files: Assistant/WebApp.hs Assistant/WebApp/* templates/* static/*
-Copyright: © 2012 Joey Hess <joey@kitenet.net>
+Copyright: © 2012-2013 Joey Hess <joey@kitenet.net>
 License: AGPL-3+
+
+Files: Utility/ThreadScheduler.hs
+Copyright: 2011 Bas van Dijk & Roel van Dijk
+           2012 Joey Hess <joey@kitenet.net>
+License: GPL-3+
+
+Files: Utility/Gpg/Types.hs
+Copyright: 2013 guilhem <guilhem@fripost.org>
+License: GPL-3+
 
 Files: doc/logo* */favicon.ico standalone/osx/git-annex.app/Contents/Resources/git-annex.icns
 Copyright: 2007 Henrik Nyh <http://henrik.nyh.se/>
diff --git a/Command/Add.hs b/Command/Add.hs
--- a/Command/Add.hs
+++ b/Command/Add.hs
@@ -109,11 +109,10 @@
 	backend <- chooseBackend $ keyFilename source
 	k <- genKey source backend
 	cache <- liftIO $ genInodeCache $ contentLocation source
-	case inodeCache source of
-		Nothing -> go k cache
-		Just c
-			| (Just c == cache) -> go k cache
-			| otherwise -> failure
+	case (cache, inodeCache source) of
+		(_, Nothing) -> go k cache
+		(Just newc, Just c) | compareStrong c newc -> go k cache
+		_ -> failure
   where
 	go k cache = ifM isDirect ( godirect k cache , goindirect k cache )
 
@@ -126,11 +125,7 @@
 
 	godirect (Just (key, _)) (Just cache) = do
 		writeInodeCache key cache
-		void $ addAssociatedFile key $ keyFilename source
-		unlessM crippledFileSystem $
-			liftIO $ allowWrite $ keyFilename source
-		when (contentLocation source /= keyFilename source) $
-			liftIO $ nukeFile $ contentLocation source
+		finishIngestDirect key source
 		return $ Just key
 	godirect _ _ = failure
 
@@ -138,6 +133,14 @@
 		when (contentLocation source /= keyFilename source) $
 			liftIO $ nukeFile $ contentLocation source
 		return Nothing		
+
+finishIngestDirect :: Key -> KeySource -> Annex ()
+finishIngestDirect key source = do
+	void $ addAssociatedFile key $ keyFilename source
+	unlessM crippledFileSystem $
+		liftIO $ allowWrite $ keyFilename source
+	when (contentLocation source /= keyFilename source) $
+		liftIO $ nukeFile $ contentLocation source
 
 perform :: FilePath -> CommandPerform
 perform file = 
diff --git a/Command/AddUrl.hs b/Command/AddUrl.hs
--- a/Command/AddUrl.hs
+++ b/Command/AddUrl.hs
@@ -25,7 +25,7 @@
 import Annex.Content.Direct
 
 def :: [Command]
-def = [notBareRepo $ withOptions [fileOption, pathdepthOption] $
+def = [notBareRepo $ withOptions [fileOption, pathdepthOption, relaxedOption] $
 	command "addurl" (paramRepeating paramUrl) seek "add urls to annex"]
 
 fileOption :: Option
@@ -34,38 +34,46 @@
 pathdepthOption :: Option
 pathdepthOption = Option.field [] "pathdepth" paramNumber "path components to use in filename"
 
+relaxedOption :: Option
+relaxedOption = Option.flag [] "relaxed" "skip size check"
+
 seek :: [CommandSeek]
 seek = [withField fileOption return $ \f ->
+	withFlag relaxedOption $ \relaxed ->
 	withField pathdepthOption (return . maybe Nothing readish) $ \d ->
-	withStrings $ start f d]
+	withStrings $ start relaxed f d]
 
-start :: Maybe FilePath -> Maybe Int -> String -> CommandStart
-start optfile pathdepth s = go $ fromMaybe bad $ parseURI s
+start :: Bool -> Maybe FilePath -> Maybe Int -> String -> CommandStart
+start relaxed optfile pathdepth s = go $ fromMaybe bad $ parseURI s
   where
 	bad = fromMaybe (error $ "bad url " ++ s) $
 		parseURI $ escapeURIString isUnescapedInURI s
 	go url = do
 		let file = fromMaybe (url2file url pathdepth) optfile
 		showStart "addurl" file
-		next $ perform s file
+		next $ perform relaxed s file
 
-perform :: String -> FilePath -> CommandPerform
-perform url file = ifAnnexed file addurl geturl
+perform :: Bool -> String -> FilePath -> CommandPerform
+perform relaxed url file = ifAnnexed file addurl geturl
   where
 	geturl = do
 		liftIO $ createDirectoryIfMissing True (parentDir file)
-		ifM (Annex.getState Annex.fast)
-			( nodownload url file , download url file )
-	addurl (key, _backend) = do
-		headers <- getHttpHeaders
-		ifM (liftIO $ Url.check url headers $ keySize key)
-			( do
-				setUrlPresent key url
-				next $ return True
-			, do
-				warning $ "failed to verify url: " ++ url
-				stop
-			)
+		ifM (Annex.getState Annex.fast <||> pure relaxed)
+			( nodownload relaxed url file , download url file )
+	addurl (key, _backend)
+		| relaxed = do
+			setUrlPresent key url
+			next $ return True
+		| otherwise = do
+			headers <- getHttpHeaders
+			ifM (liftIO $ Url.check url headers $ keySize key)
+				( do
+					setUrlPresent key url
+					next $ return True
+				, do
+					warning $ "failed to verify url: " ++ url
+					stop
+				)
 
 download :: String -> FilePath -> CommandPerform
 download url file = do
@@ -90,10 +98,12 @@
 				setUrlPresent key url
 				next $ Command.Add.cleanup file key True
 
-nodownload :: String -> FilePath -> CommandPerform
-nodownload url file = do
+nodownload :: Bool -> String -> FilePath -> CommandPerform
+nodownload relaxed url file = do
 	headers <- getHttpHeaders
-	(exists, size) <- liftIO $ Url.exists url headers
+	(exists, size) <- if relaxed
+		then pure (True, Nothing)
+		else liftIO $ Url.exists url headers
 	if exists
 		then do
 			let key = Backend.URL.fromUrl url size
diff --git a/Command/Assistant.hs b/Command/Assistant.hs
--- a/Command/Assistant.hs
+++ b/Command/Assistant.hs
@@ -50,21 +50,17 @@
 
 autoStart :: IO ()
 autoStart = do
-	autostartfile <- autoStartFile
-	let nothing = error $ "Nothing listed in " ++ autostartfile
-	ifM (doesFileExist autostartfile)
-		( do
-			dirs <- nub . lines <$> readFile autostartfile
-			program <- readProgramFile
-			when (null dirs) nothing
-			forM_ dirs $ \d -> do
-				putStrLn $ "git-annex autostart in " ++ d
-				ifM (catchBoolIO $ go program d)
-					( putStrLn "ok"
-					, putStrLn "failed"
-					)
-		, nothing
-		)
+	dirs <- liftIO readAutoStartFile
+	when (null dirs) $ do
+		f <- autoStartFile
+		error $ "Nothing listed in " ++ f
+	program <- readProgramFile
+	forM_ dirs $ \d -> do
+		putStrLn $ "git-annex autostart in " ++ d
+		ifM (catchBoolIO $ go program d)
+			( putStrLn "ok"
+			, putStrLn "failed"
+			)
   where
 	go program dir = do
 		changeWorkingDirectory dir
diff --git a/Command/Copy.hs b/Command/Copy.hs
--- a/Command/Copy.hs
+++ b/Command/Copy.hs
@@ -18,8 +18,8 @@
 	"copy content of files to/from another repository"]
 
 seek :: [CommandSeek]
-seek = [withField Command.Move.toOption Remote.byName $ \to ->
-		withField Command.Move.fromOption Remote.byName $ \from ->
+seek = [withField Command.Move.toOption Remote.byNameWithUUID $ \to ->
+		withField Command.Move.fromOption Remote.byNameWithUUID $ \from ->
 			withFilesInGit $ whenAnnexed $ start to from]
 
 {- A copy is just a move that does not delete the source file.
diff --git a/Command/Direct.hs b/Command/Direct.hs
--- a/Command/Direct.hs
+++ b/Command/Direct.hs
@@ -30,8 +30,12 @@
 perform = do
 	showStart "commit" ""
 	showOutput
-	_ <- inRepo $ Git.Command.runBool "commit"
-		[Param "-a", Param "-m", Param "commit before switching to direct mode"]
+	_ <- inRepo $ Git.Command.runBool
+		[ Param "commit"
+		, Param "-a"
+		, Param "-m"
+		, Param "commit before switching to direct mode"
+		]
 	showEndOk
 
 	top <- fromRepo Git.repoPath
diff --git a/Command/Drop.hs b/Command/Drop.hs
--- a/Command/Drop.hs
+++ b/Command/Drop.hs
@@ -27,7 +27,7 @@
 fromOption = Option.field ['f'] "from" paramRemote "drop content from a remote"
 
 seek :: [CommandSeek]
-seek = [withField fromOption Remote.byName $ \from ->
+seek = [withField fromOption Remote.byNameWithUUID $ \from ->
 	withFilesInGit $ whenAnnexed $ start from]
 
 start :: Maybe Remote -> FilePath -> (Key, Backend) -> CommandStart
@@ -89,10 +89,8 @@
 
 cleanupRemote :: Key -> Remote -> Bool -> CommandCleanup
 cleanupRemote key remote ok = do
-	-- better safe than sorry: assume the remote dropped the key
-	-- even if it seemed to fail; the failure could have occurred
-	-- after it really dropped it
-	Remote.logStatus remote key InfoMissing
+	when ok $
+		Remote.logStatus remote key InfoMissing
 	return ok
 
 {- Checks specified remotes to verify that enough copies of a key exist to
diff --git a/Command/DropUnused.hs b/Command/DropUnused.hs
--- a/Command/DropUnused.hs
+++ b/Command/DropUnused.hs
@@ -28,7 +28,7 @@
 start = startUnused "dropunused" perform (performOther gitAnnexBadLocation) (performOther gitAnnexTmpLocation)
 
 perform :: Key -> CommandPerform
-perform key = maybe droplocal dropremote =<< Remote.byName =<< from
+perform key = maybe droplocal dropremote =<< Remote.byNameWithUUID =<< from
   where
 	dropremote r = do
 		showAction $ "from " ++ Remote.name r
diff --git a/Command/Fsck.hs b/Command/Fsck.hs
--- a/Command/Fsck.hs
+++ b/Command/Fsck.hs
@@ -61,7 +61,7 @@
 
 seek :: [CommandSeek]
 seek =
-	[ withField fromOption Remote.byName $ \from ->
+	[ withField fromOption Remote.byNameWithUUID $ \from ->
 	  withIncremental $ \i -> withFilesInGit $ whenAnnexed $ start from i
 	, withIncremental $ \i -> withBarePresentKeys $ startBare i
 	]
@@ -401,10 +401,8 @@
 badContentRemote :: Remote -> Key -> Annex String
 badContentRemote remote key = do
 	ok <- Remote.removeKey remote key
-	-- better safe than sorry: assume the remote dropped the key
-	-- even if it seemed to fail; the failure could have occurred
-	-- after it really dropped it
-	Remote.logStatus remote key InfoMissing
+	when ok $
+		Remote.logStatus remote key InfoMissing
 	return $ (if ok then "dropped from " else "failed to drop from ")
 		++ Remote.name remote
 
diff --git a/Command/Get.hs b/Command/Get.hs
--- a/Command/Get.hs
+++ b/Command/Get.hs
@@ -20,7 +20,7 @@
 	"make content of annexed files available"]
 
 seek :: [CommandSeek]
-seek = [withField Command.Move.fromOption Remote.byName $ \from ->
+seek = [withField Command.Move.fromOption Remote.byNameWithUUID $ \from ->
 	withFilesInGit $ whenAnnexed $ start from]
 
 start :: Maybe Remote -> FilePath -> (Key, Backend) -> CommandStart
diff --git a/Command/Indirect.hs b/Command/Indirect.hs
--- a/Command/Indirect.hs
+++ b/Command/Indirect.hs
@@ -43,8 +43,11 @@
 	showStart "commit" ""
 	whenM (stageDirect) $ do
 		showOutput
-		void $ inRepo $ Git.Command.runBool "commit"
-			[Param "-m", Param "commit before switching to indirect mode"]
+		void $ inRepo $ Git.Command.runBool
+			[ Param "commit"
+			, Param "-m"
+			, Param "commit before switching to indirect mode"
+			]
 	showEndOk
 
 	-- Note that we set indirect mode early, so that we can use
diff --git a/Command/Move.hs b/Command/Move.hs
--- a/Command/Move.hs
+++ b/Command/Move.hs
@@ -32,8 +32,8 @@
 options = [fromOption, toOption]
 
 seek :: [CommandSeek]
-seek = [withField toOption Remote.byName $ \to ->
-		withField fromOption Remote.byName $ \from ->
+seek = [withField toOption Remote.byNameWithUUID $ \to ->
+		withField fromOption Remote.byNameWithUUID $ \from ->
 			withFilesInGit $ whenAnnexed $ start to from True]
 
 start :: Maybe Remote -> Maybe Remote -> Bool -> FilePath -> (Key, Backend) -> CommandStart
diff --git a/Command/Status.hs b/Command/Status.hs
--- a/Command/Status.hs
+++ b/Command/Status.hs
@@ -34,6 +34,7 @@
 import Utility.Percentage
 import Logs.Transfer
 import Types.TrustLevel
+import qualified Limit
 
 -- a named computation that produces a statistic
 type Stat = StatState (Maybe (String, StatState String))
@@ -56,17 +57,43 @@
 type StatState = StateT StatInfo Annex
 
 def :: [Command]
-def = [command "status" paramNothing seek
+def = [command "status" (paramOptional paramPaths) seek
 	"shows status information about the annex"]
 
 seek :: [CommandSeek]
-seek = [withNothing start]
+seek = [withWords start]
 
+start :: [FilePath] -> CommandStart
+start [] = do
+	globalStatus
+	stop
+start ps = do
+	mapM_ localStatus =<< filterM isdir ps
+	stop
+  where
+	isdir = liftIO . catchBoolIO . (isDirectory <$$> getFileStatus)
+
+globalStatus :: Annex ()
+globalStatus = do
+	fast <- Annex.getState Annex.fast
+	let stats = if fast
+		then global_fast_stats
+		else global_fast_stats ++ global_slow_stats
+	showCustom "status" $ do
+		evalStateT (mapM_ showStat stats) (StatInfo Nothing Nothing)
+		return True
+
+localStatus :: FilePath -> Annex ()
+localStatus dir = showCustom (unwords ["status", dir]) $ do
+	let stats = map (\s -> s dir) local_stats
+	evalStateT (mapM_ showStat stats) =<< getLocalStatInfo dir
+	return True
+
 {- Order is significant. Less expensive operations, and operations
  - that share data go together.
  -}
-fast_stats :: [Stat]
-fast_stats = 
+global_fast_stats :: [Stat]
+global_fast_stats = 
 	[ supported_backends
 	, supported_remote_types
 	, repository_mode
@@ -77,8 +104,8 @@
 	, transfer_list
 	, disk_size
 	]
-slow_stats :: [Stat]
-slow_stats = 
+global_slow_stats :: [Stat]
+global_slow_stats = 
 	[ tmp_size
 	, bad_data_size
 	, local_annex_keys
@@ -88,15 +115,14 @@
 	, bloom_info
 	, backend_usage
 	]
-
-start :: CommandStart
-start = do
-	fast <- Annex.getState Annex.fast
-	let stats = if fast then fast_stats else fast_stats ++ slow_stats
-	showCustom "status" $ do
-		evalStateT (mapM_ showStat stats) (StatInfo Nothing Nothing)
-		return True
-	stop
+local_stats :: [FilePath -> Stat]
+local_stats =
+	[ local_dir
+	, const local_annex_keys
+	, const local_annex_size
+	, const known_annex_keys
+	, const known_annex_size
+	]
 
 stat :: String -> (String -> StatState String) -> Stat
 stat desc a = return $ Just (desc, a desc)
@@ -142,6 +168,9 @@
   where
 	n = showTrustLevel level ++ " repositories"
 	
+local_dir :: FilePath -> Stat
+local_dir dir = stat "directory" $ json id $ return dir
+
 local_annex_size :: Stat
 local_annex_size = stat "local annex size" $ json id $
 	showSizeKeys <$> cachedPresentData
@@ -246,6 +275,26 @@
 			put s { referencedData = Just v }
 			return v
 
+getLocalStatInfo :: FilePath -> Annex StatInfo
+getLocalStatInfo dir = do
+	matcher <- Limit.getMatcher
+	(presentdata, referenceddata) <-
+		Command.Unused.withKeysFilesReferencedIn dir initial
+			(update matcher)
+	return $ StatInfo (Just presentdata) (Just referenceddata)
+  where
+	initial = (emptyKeyData, emptyKeyData)
+	update matcher key file vs@(presentdata, referenceddata) =
+		ifM (matcher $ Annex.FileInfo file file)
+			( (,)
+				<$> ifM (inAnnex key)
+					( return $ addKey key presentdata
+					, return presentdata
+					)
+				<*> pure (addKey key referenceddata)
+			, return vs
+			)
+
 emptyKeyData :: KeyData
 emptyKeyData = KeyData 0 0 0 M.empty
 
@@ -293,4 +342,3 @@
 
 multiLine :: [String] -> String
 multiLine = concatMap (\l -> "\n\t" ++ l)
-
diff --git a/Command/Sync.hs b/Command/Sync.hs
--- a/Command/Sync.hs
+++ b/Command/Sync.hs
@@ -91,7 +91,7 @@
 		showOutput
 		Annex.Branch.commit "update"
 		-- Commit will fail when the tree is clean, so ignore failure.
-		_ <- inRepo $ Git.Command.runBool "commit" $ ps ++
+		_ <- inRepo $ Git.Command.runBool $ (Param "commit") : ps ++
 			[Param "-m", Param "git-annex automatic sync"]
 		return True
 
@@ -117,8 +117,9 @@
 updateBranch syncbranch g = 
 	unlessM go $ error $ "failed to update " ++ show syncbranch
   where
-	go = Git.Command.runBool "branch"
-		[ Param "-f"
+	go = Git.Command.runBool
+		[ Param "branch"
+		, Param "-f"
 		, Param $ show $ Git.Ref.base syncbranch
 		] g
 
@@ -130,8 +131,8 @@
 		stopUnless fetch $
 			next $ mergeRemote remote (Just branch)
   where
-	fetch = inRepo $ Git.Command.runBool "fetch"
-		[Param $ Remote.name remote]
+	fetch = inRepo $ Git.Command.runBool
+		[Param "fetch", Param $ Remote.name remote]
 
 {- The remote probably has both a master and a synced/master branch.
  - Which to merge from? Well, the master has whatever latest changes
@@ -162,8 +163,9 @@
 
 pushBranch :: Remote -> Git.Ref -> Git.Repo -> IO Bool
 pushBranch remote branch g =
-	Git.Command.runBool "push"
-		[ Param $ Remote.name remote
+	Git.Command.runBool
+		[ Param "push"
+		, Param $ Remote.name remote
 		, Param $ refspec Annex.Branch.name
 		, Param $ refspec branch
 		] g
@@ -233,8 +235,11 @@
 	
 	when merged $ do
 		Annex.Queue.flush
-		void $ inRepo $ Git.Command.runBool "commit"
-			[Param "-m", Param "git-annex automatic merge conflict fix"]
+		void $ inRepo $ Git.Command.runBool
+			[ Param "commit"
+			, Param "-m"
+			, Param "git-annex automatic merge conflict fix"
+			]
 	return merged
 
 resolveMerge' :: LsFiles.Unmerged -> Annex Bool
diff --git a/Command/TransferKey.hs b/Command/TransferKey.hs
--- a/Command/TransferKey.hs
+++ b/Command/TransferKey.hs
@@ -29,8 +29,8 @@
 fileOption = Option.field [] "file" paramFile "the associated file"
 
 seek :: [CommandSeek]
-seek = [withField Command.Move.toOption Remote.byName $ \to ->
-	withField Command.Move.fromOption Remote.byName $ \from ->
+seek = [withField Command.Move.toOption Remote.byNameWithUUID $ \to ->
+	withField Command.Move.fromOption Remote.byNameWithUUID $ \from ->
 	withField fileOption return $ \file ->
 		withKeys $ start to from file]
 
diff --git a/Command/Unannex.hs b/Command/Unannex.hs
--- a/Command/Unannex.hs
+++ b/Command/Unannex.hs
@@ -34,7 +34,7 @@
 cleanup file key = do
 	liftIO $ removeFile file
 	-- git rm deletes empty directory without --cached
-	inRepo $ Git.Command.run "rm" [Params "--cached --quiet --", File file]
+	inRepo $ Git.Command.run [Params "rm --cached --quiet --", File file]
 	
 	-- If the file was already committed, it is now staged for removal.
 	-- Commit that removal now, to avoid later confusing the
@@ -42,10 +42,12 @@
 	-- git as a normal, non-annexed file.
 	(s, clean) <- inRepo $ LsFiles.staged [file]
 	when (not $ null s) $ do
-		inRepo $ Git.Command.run "commit" [
-			Param "-q",
-			Params "-m", Param "content removed from git annex",
-			Param "--", File file]
+		inRepo $ Git.Command.run
+			[ Param "commit"
+			, Param "-q"
+			, Param "-m", Param "content removed from git annex"
+			, Param "--", File file
+			]
 	void $ liftIO clean
 
 	ifM (Annex.getState Annex.fast)
diff --git a/Command/Uninit.hs b/Command/Uninit.hs
--- a/Command/Uninit.hs
+++ b/Command/Uninit.hs
@@ -67,6 +67,6 @@
 	liftIO $ removeDirectoryRecursive annexdir
 	-- avoid normal shutdown
 	saveState False
-	inRepo $ Git.Command.run "branch"
-		[Param "-D", Param $ show Annex.Branch.name]
+	inRepo $ Git.Command.run
+		[Param "branch", Param "-D", Param $ show Annex.Branch.name]
 	liftIO exitSuccess
diff --git a/Command/Unused.hs b/Command/Unused.hs
--- a/Command/Unused.hs
+++ b/Command/Unused.hs
@@ -76,7 +76,7 @@
 		chain v' as
 
 checkRemoteUnused :: String -> CommandPerform
-checkRemoteUnused name = go =<< fromJust <$> Remote.byName (Just name)
+checkRemoteUnused name = go =<< fromJust <$> Remote.byNameWithUUID (Just name)
   where
 	go r = do
 		showAction "checking for unused data"
@@ -213,36 +213,42 @@
 {- Given an initial value, folds it with each key referenced by
  - symlinks in the git repo. -}
 withKeysReferenced :: v -> (Key -> v -> v) -> Annex v
-withKeysReferenced initial a = withKeysReferenced' initial folda
+withKeysReferenced initial a = withKeysReferenced' Nothing initial folda
   where
-	folda k v = return $ a k v
+	folda k _ v = return $ a k v
 
 {- Runs an action on each referenced key in the git repo. -}
 withKeysReferencedM :: (Key -> Annex ()) -> Annex ()
-withKeysReferencedM a = withKeysReferenced' () calla
+withKeysReferencedM a = withKeysReferenced' Nothing () calla
   where
-	calla k _ = a k
+	calla k _ _ = a k
 
-withKeysReferenced' :: v -> (Key -> v -> Annex v) -> Annex v
-withKeysReferenced' initial a = do
+{- Folds an action over keys and files referenced in a particular directory. -}
+withKeysFilesReferencedIn :: FilePath -> v -> (Key -> FilePath -> v -> Annex v) -> Annex v
+withKeysFilesReferencedIn = withKeysReferenced' . Just
+
+withKeysReferenced' :: Maybe FilePath -> v -> (Key -> FilePath -> v -> Annex v) -> Annex v
+withKeysReferenced' mdir initial a = do
 	(files, clean) <- getfiles
 	r <- go initial files
 	liftIO $ void clean
 	return r
   where
-	getfiles = ifM isBareRepo
-		( return ([], return True)
-		, do
-			top <- fromRepo Git.repoPath
-			inRepo $ LsFiles.inRepo [top]
-		)
+	getfiles = case mdir of
+		Nothing -> ifM isBareRepo
+			( return ([], return True)
+			, do
+				top <- fromRepo Git.repoPath
+				inRepo $ LsFiles.inRepo [top]
+			)
+		Just dir -> inRepo $ LsFiles.inRepo [dir]
 	go v [] = return v
 	go v (f:fs) = do
 		x <- Backend.lookupFile f
 		case x of
 			Nothing -> go v fs
 			Just (k, _) -> do
-				!v' <- a k v
+				!v' <- a k f v
 				go v' fs
 
 withKeysReferencedInGit :: (Key -> Annex ()) -> Annex ()
diff --git a/Command/Version.hs b/Command/Version.hs
--- a/Command/Version.hs
+++ b/Command/Version.hs
@@ -11,6 +11,7 @@
 import Command
 import qualified Build.SysConfig as SysConfig
 import Annex.Version
+import BuildFlags
 
 def :: [Command]
 def = [noCommit $ noRepo showPackageVersion $ dontCheck repoExists $
@@ -28,6 +29,7 @@
 		putStrLn $ "default repository version: " ++ defaultVersion
 		putStrLn $ "supported repository versions: " ++ vs supportedVersions
 		putStrLn $ "upgrade supported from repository versions: " ++ vs upgradableVersions
+		putStrLn $ "build flags: " ++ unwords buildFlags
 	stop
   where
 	vs = join " "
diff --git a/Command/WebApp.hs b/Command/WebApp.hs
--- a/Command/WebApp.hs
+++ b/Command/WebApp.hs
@@ -64,20 +64,13 @@
 		liftIO $ isJust <$> checkDaemon pidfile
 	checkshim f = liftIO $ doesFileExist f
 
-{- When run without a repo, see if there is an autoStartFile,
- - and if so, start the first available listed repository.
- - If not, it's our first time being run! -}
+{- When run without a repo, start the first available listed repository in
+ - the autostart file. If not, it's our first time being run! -}
 startNoRepo :: IO ()
 startNoRepo = do
-	autostartfile <- autoStartFile
-	ifM (doesFileExist autostartfile) ( autoStart autostartfile , firstRun )
-
-autoStart :: FilePath -> IO ()
-autoStart autostartfile = do
-	dirs <- nub . lines <$> readFile autostartfile
-	edirs <- filterM doesDirectoryExist dirs
-	case edirs of
-		[] -> firstRun -- what else can I do? Nothing works..
+	dirs <- liftIO $ filterM doesDirectoryExist =<< readAutoStartFile
+	case dirs of
+		[] -> firstRun
 		(d:_) -> do
 			changeWorkingDirectory d
 			state <- Annex.new =<< Git.CurrentRepo.get
diff --git a/Config.hs b/Config.hs
--- a/Config.hs
+++ b/Config.hs
@@ -12,6 +12,8 @@
 import qualified Git.Config
 import qualified Git.Command
 import qualified Annex
+import qualified Types.Remote as Remote
+import Config.Cost
 
 type UnqualifiedConfigKey = String
 data ConfigKey = ConfigKey String
@@ -23,13 +25,13 @@
 {- Changes a git config setting in both internal state and .git/config -}
 setConfig :: ConfigKey -> String -> Annex ()
 setConfig (ConfigKey key) value = do
-	inRepo $ Git.Command.run "config" [Param key, Param value]
+	inRepo $ Git.Command.run [Param "config", Param key, Param value]
 	Annex.changeGitRepo =<< inRepo Git.Config.reRead
 
 {- Unsets a git config setting. (Leaves it in state currently.) -}
 unsetConfig :: ConfigKey -> Annex ()
-unsetConfig (ConfigKey key) = inRepo $ Git.Command.run "config"
-	[Param "--unset", Param key]
+unsetConfig (ConfigKey key) = inRepo $ Git.Command.run
+	[Param "config", Param "--unset", Param key]
 
 {- A per-remote config setting in git config. -}
 remoteConfig :: Git.Repo -> UnqualifiedConfigKey -> ConfigKey
@@ -43,36 +45,15 @@
 {- Calculates cost for a remote. Either the specific default, or as configured 
  - by remote.<name>.annex-cost, or if remote.<name>.annex-cost-command
  - is set and prints a number, that is used. -}
-remoteCost :: RemoteGitConfig -> Int -> Annex Int
+remoteCost :: RemoteGitConfig -> Cost -> Annex Cost
 remoteCost c def = case remoteAnnexCostCommand c of
 	Just cmd | not (null cmd) -> liftIO $
 		(fromMaybe def . readish) <$>
 			readProcess "sh" ["-c", cmd]
 	_ -> return $ fromMaybe def $ remoteAnnexCost c
 
-cheapRemoteCost :: Int
-cheapRemoteCost = 100
-semiCheapRemoteCost :: Int
-semiCheapRemoteCost = 110
-expensiveRemoteCost :: Int
-expensiveRemoteCost = 200
-veryExpensiveRemoteCost :: Int
-veryExpensiveRemoteCost = 1000
-
-{- Adjusts a remote's cost to reflect it being encrypted. -}
-encryptedRemoteCostAdj :: Int
-encryptedRemoteCostAdj = 50
-
-{- Make sure the remote cost numbers work out. -}
-prop_cost_sane :: Bool
-prop_cost_sane = False `notElem`
-	[ expensiveRemoteCost > 0
-	, cheapRemoteCost < semiCheapRemoteCost
-	, semiCheapRemoteCost < expensiveRemoteCost
-	, cheapRemoteCost + encryptedRemoteCostAdj > semiCheapRemoteCost
-	, cheapRemoteCost + encryptedRemoteCostAdj < expensiveRemoteCost
-	, semiCheapRemoteCost + encryptedRemoteCostAdj < expensiveRemoteCost
-	]
+setRemoteCost :: Remote -> Cost -> Annex ()
+setRemoteCost r c = setConfig (remoteConfig (Remote.repo r) "cost") (show c)
 
 getNumCopies :: Maybe Int -> Annex Int
 getNumCopies (Just v) = return v
diff --git a/Config/Cost.hs b/Config/Cost.hs
new file mode 100644
--- /dev/null
+++ b/Config/Cost.hs
@@ -0,0 +1,82 @@
+{- Remote costs.
+ -
+ - Copyright 2011-2013 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Config.Cost where
+
+{- We use a float for a cost to ensure that there is a cost in
+ - between any two other costs. -}
+type Cost = Float
+
+{- Some predefined default costs.
+ - Users setting costs in config files can be aware of these,
+ - and pick values relative to them. So don't change. -}
+cheapRemoteCost :: Cost
+cheapRemoteCost = 100
+nearlyCheapRemoteCost :: Cost
+nearlyCheapRemoteCost = 110
+semiExpensiveRemoteCost :: Cost
+semiExpensiveRemoteCost = 175
+expensiveRemoteCost :: Cost
+expensiveRemoteCost = 200
+veryExpensiveRemoteCost :: Cost
+veryExpensiveRemoteCost = 1000
+
+{- Adjusts a remote's cost to reflect it being encrypted. -}
+encryptedRemoteCostAdj :: Cost
+encryptedRemoteCostAdj = 50
+
+{- Given an ordered list of costs, and the position of one of the items
+ - the list, inserts a new cost into the list, in between the item
+ - and the item after it.
+ -
+ - If two or move items have the same cost, their costs are adjusted
+ - to make room. The costs of other items in the list are left
+ - unchanged.
+ -
+ - To insert the new cost before any other in the list, specify a negative
+ - position. To insert the new cost at the end of the list, specify a
+ - position longer than the list.
+ -}
+insertCostAfter :: [Cost] -> Int -> [Cost]
+insertCostAfter [] _ = error "insertCostAfter: empty list"
+insertCostAfter l pos
+	| pos < 0 = costBetween 0 (l !! 0) : l
+	| nextpos > maxpos = l ++ [1 + l !! maxpos]
+	| item == nextitem =
+		let (_dup:new:l') = insertCostAfter lastsegment 0
+		in firstsegment ++ [costBetween item new, new] ++ l'
+	| otherwise =
+		firstsegment ++ [costBetween item nextitem ] ++ lastsegment
+  where
+  	nextpos = pos + 1
+	maxpos = length l - 1
+	
+	item = l !! pos
+	nextitem = l !! nextpos
+		
+	(firstsegment, lastsegment) = splitAt (pos + 1) l
+
+costBetween :: Cost -> Cost -> Cost
+costBetween x y
+	| x == y = x
+	| x > y = -- avoid fractions unless needed
+		let mid = y + (x - y) / 2
+		    mid' = fromIntegral ((floor mid) :: Int)
+		in if mid' > y then mid' else mid
+	| otherwise = costBetween y x
+
+{- Make sure the remote cost numbers work out. -}
+prop_cost_sane :: Bool
+prop_cost_sane = False `notElem`
+	[ expensiveRemoteCost > 0
+	, cheapRemoteCost < nearlyCheapRemoteCost
+	, nearlyCheapRemoteCost < semiExpensiveRemoteCost
+	, semiExpensiveRemoteCost < expensiveRemoteCost
+	, cheapRemoteCost + encryptedRemoteCostAdj > nearlyCheapRemoteCost
+	, nearlyCheapRemoteCost + encryptedRemoteCostAdj < semiExpensiveRemoteCost
+	, nearlyCheapRemoteCost + encryptedRemoteCostAdj < expensiveRemoteCost
+	]
diff --git a/Creds.hs b/Creds.hs
--- a/Creds.hs
+++ b/Creds.hs
@@ -48,7 +48,7 @@
 		return c
 
 	storeconfig creds key (Just cipher) = do
-		s <- liftIO $ encrypt cipher
+		s <- liftIO $ encrypt (GpgOpts []) cipher
 			(feedBytes $ L.pack $ encodeCredPair creds)
 			(readBytes $ return . L.unpack)
 		return $ M.insert key (toB64 s) c
diff --git a/Crypto.hs b/Crypto.hs
--- a/Crypto.hs
+++ b/Crypto.hs
@@ -23,6 +23,8 @@
 	readBytes,
 	encrypt,
 	decrypt,	
+	GpgOpts(..),
+	getGpgOpts,
 
 	prop_hmacWithCipher_sane
 ) where
@@ -34,31 +36,34 @@
 
 import Common.Annex
 import qualified Utility.Gpg as Gpg
+import Utility.Gpg.Types
 import Types.Key
 import Types.Crypto
 
-{- The first half of a Cipher is used for HMAC; the remainder
+{- The beginning of a Cipher is used for HMAC; the remainder
  - is used as the GPG symmetric encryption passphrase.
  -
- - HMAC SHA1 needs only 64 bytes. The remainder is for expansion,
+ - HMAC SHA1 needs only 64 bytes. The rest of the HMAC key is for expansion,
  - perhaps to HMAC SHA512, which needs 128 bytes (ideally).
+ - It also provides room the Cipher to contain data in a form like base64,
+ - which does not pack a full byte of entropy into a byte of data.
  -
- - 256 is enough for gpg's symetric cipher; unlike weaker public key
+ - 256 bytes is enough for gpg's symetric cipher; unlike weaker public key
  - crypto, the key does not need to be too large.
  -}
-cipherHalf :: Int
-cipherHalf = 256
+cipherBeginning :: Int
+cipherBeginning = 256
 
 cipherSize :: Int
-cipherSize = cipherHalf * 2
+cipherSize = 512
 
 cipherPassphrase :: Cipher -> String
-cipherPassphrase (Cipher c) = drop cipherHalf c
+cipherPassphrase (Cipher c) = drop cipherBeginning c
 
 cipherHmac :: Cipher -> String
-cipherHmac (Cipher c) = take cipherHalf c
+cipherHmac (Cipher c) = take cipherBeginning c
 
-{- Creates a new Cipher, encrypted to the specificed key id. -}
+{- Creates a new Cipher, encrypted to the specified key id. -}
 genEncryptedCipher :: String -> IO StorableCipher
 genEncryptedCipher keyid = do
 	ks <- Gpg.findPubKeys keyid
@@ -90,7 +95,8 @@
 {- Encrypts a Cipher to the specified KeyIds. -}
 encryptCipher :: Cipher -> KeyIds -> IO StorableCipher
 encryptCipher (Cipher c) (KeyIds ks) = do
-	let ks' = nub $ sort ks -- gpg complains about duplicate recipient keyids
+	-- gpg complains about duplicate recipient keyids
+	let ks' = nub $ sort ks
 	encipher <- Gpg.pipeStrict ([ Params "--encrypt" ] ++ recipients ks') c
 	return $ EncryptedCipher encipher (KeyIds ks')
   where
@@ -103,7 +109,8 @@
 {- Decrypting an EncryptedCipher is expensive; the Cipher should be cached. -}
 decryptCipher :: StorableCipher -> IO Cipher
 decryptCipher (SharedCipher t) = return $ Cipher t
-decryptCipher (EncryptedCipher t _) = Cipher <$> Gpg.pipeStrict [ Param "--decrypt" ] t
+decryptCipher (EncryptedCipher t _) =
+	Cipher <$> Gpg.pipeStrict [ Param "--decrypt" ] t
 
 {- Generates an encrypted form of a Key. The encryption does not need to be
  - reversable, nor does it need to be the same type of encryption used
@@ -128,10 +135,12 @@
 readBytes :: (L.ByteString -> IO a) -> Reader a
 readBytes a h = L.hGetContents h >>= a
 
-{- Runs a Feeder action, that generates content that is encrypted with the
- - Cipher, and read by the Reader action. -}
-encrypt :: Cipher -> Feeder -> Reader a -> IO a
-encrypt = Gpg.feedRead [Params "--symmetric --force-mdc"] . cipherPassphrase
+{- Runs a Feeder action, that generates content that is symmetrically encrypted
+ - with the Cipher using the given GnuPG options, and then read by the Reader
+ - action. -}
+encrypt :: GpgOpts -> Cipher -> Feeder -> Reader a -> IO a
+encrypt opts = Gpg.feedRead ( Params "--symmetric --force-mdc" : toParams opts )
+		. cipherPassphrase
 
 {- Runs a Feeder action, that generates content that is decrypted with the
  - Cipher, and read by the Reader action. -}
diff --git a/Git/Branch.hs b/Git/Branch.hs
--- a/Git/Branch.hs
+++ b/Git/Branch.hs
@@ -73,8 +73,7 @@
   where
 	no_ff = return False
 	do_ff to = do
-		run "update-ref"
-			[Param $ show branch, Param $ show to] repo
+		run [Param "update-ref", Param $ show branch, Param $ show to] repo
 		return True
 	findbest c [] = return $ Just c
 	findbest c (r:rs)
@@ -97,7 +96,7 @@
 	sha <- getSha "commit-tree" $ pipeWriteRead
 		(map Param $ ["commit-tree", show tree] ++ ps)
 		message repo
-	run "update-ref" [Param $ show branch, Param $ show sha] repo
+	run [Param "update-ref", Param $ show branch, Param $ show sha] repo
 	return sha
   where
 	ps = concatMap (\r -> ["-p", show r]) parentrefs
diff --git a/Git/Command.hs b/Git/Command.hs
--- a/Git/Command.hs
+++ b/Git/Command.hs
@@ -25,19 +25,25 @@
 gitCommandLine _ repo = assertLocal repo $ error "internal"
 
 {- Runs git in the specified repo. -}
-runBool :: String -> [CommandParam] -> Repo -> IO Bool
-runBool subcommand params repo = assertLocal repo $
+runBool :: [CommandParam] -> Repo -> IO Bool
+runBool params repo = assertLocal repo $
 	boolSystemEnv "git"
-		(gitCommandLine (Param subcommand : params) repo)
+		(gitCommandLine params repo)
 		(gitEnv repo)
 
 {- Runs git in the specified repo, throwing an error if it fails. -}
-run :: String -> [CommandParam] -> Repo -> IO ()
-run subcommand params repo = assertLocal repo $
-	unlessM (runBool subcommand params repo) $
-		error $ "git " ++ subcommand ++ " " ++ show params ++ " failed"
+run :: [CommandParam] -> Repo -> IO ()
+run params repo = assertLocal repo $
+	unlessM (runBool params repo) $
+		error $ "git " ++ show params ++ " failed"
 
-{- Runs a git subcommand and returns its output, lazily.
+{- Runs git and forces it to be quiet, throwing an error if it fails. -}
+runQuiet :: [CommandParam] -> Repo -> IO ()
+runQuiet params repo = withQuietOutput createProcessSuccess $
+	(proc "git" $ toCommand $ gitCommandLine (params) repo)
+		{ env = gitEnv repo }
+
+{- Runs a git command and returns its output, lazily.
  -
  - Also returns an action that should be used when the output is all
  - read (or no more is needed), that will wait on the command, and
@@ -52,7 +58,7 @@
   where
 	p  = gitCreateProcess params repo
 
-{- Runs a git subcommand, and returns its output, strictly.
+{- Runs a git command, and returns its output, strictly.
  -
  - Nonzero exit status is ignored.
  -}
@@ -66,7 +72,7 @@
   where
 	p  = gitCreateProcess params repo
 
-{- Runs a git subcommand, feeding it input, and returning its output,
+{- Runs a git command, feeding it input, and returning its output,
  - which is expected to be fairly small, since it's all read into memory
  - strictly. -}
 pipeWriteRead :: [CommandParam] -> String -> Repo -> IO String
@@ -74,7 +80,7 @@
 	writeReadProcessEnv "git" (toCommand $ gitCommandLine params repo) 
 		(gitEnv repo) s (Just fileEncoding)
 
-{- Runs a git subcommand, feeding it input on a handle with an action. -}
+{- Runs a git command, feeding it input on a handle with an action. -}
 pipeWrite :: [CommandParam] -> Repo -> (Handle -> IO ()) -> IO ()
 pipeWrite params repo = withHandle StdinHandle createProcessSuccess $
 	gitCreateProcess params repo
diff --git a/Git/Construct.hs b/Git/Construct.hs
--- a/Git/Construct.hs
+++ b/Git/Construct.hs
@@ -18,6 +18,7 @@
 	fromRemoteLocation,
 	repoAbsPath,
 	newFrom,
+	checkForRepo,
 ) where
 
 import System.Posix.User
@@ -211,6 +212,8 @@
 		| c == '/' = (n, cs)
 		| otherwise = findname (n++[c]) cs
 
+{- Checks if a git repository exists in a directory. Does not find
+ - git repositories in parent directories. -}
 checkForRepo :: FilePath -> IO (Maybe RepoLocation)
 checkForRepo dir = 
 	check isRepo $
diff --git a/Git/LsFiles.hs b/Git/LsFiles.hs
--- a/Git/LsFiles.hs
+++ b/Git/LsFiles.hs
@@ -105,7 +105,7 @@
 	return (map (\f -> relPathDirToFile cwd $ top </> f) fs, cleanup)
   where
 	prefix = [Params "diff --name-only --diff-filter=T -z"]
-	suffix = Param "--" : map File l
+	suffix = Param "--" : (if null l then [File "."] else map File l)
 
 {- A item in conflict has two possible values.
  - Either can be Nothing, when that side deleted the file. -}
diff --git a/Git/Merge.hs b/Git/Merge.hs
--- a/Git/Merge.hs
+++ b/Git/Merge.hs
@@ -15,5 +15,7 @@
 {- Avoids recent git's interactive merge. -}
 mergeNonInteractive :: Ref -> Repo -> IO Bool
 mergeNonInteractive branch
-	| older "1.7.7.6" = runBool "merge" [Param $ show branch]
-	| otherwise = runBool "merge" [Param "--no-edit", Param $ show branch]
+	| older "1.7.7.6" = merge [Param $ show branch]
+	| otherwise = merge [Param "--no-edit", Param $ show branch]
+  where
+	merge ps = runBool $ Param "merge" : ps
diff --git a/Git/Ref.hs b/Git/Ref.hs
--- a/Git/Ref.hs
+++ b/Git/Ref.hs
@@ -34,8 +34,8 @@
 
 {- Checks if a ref exists. -}
 exists :: Ref -> Repo -> IO Bool
-exists ref = runBool "show-ref" 
-	[Param "--verify", Param "-q", Param $ show ref]
+exists ref = runBool
+	[Param "show-ref", Param "--verify", Param "-q", Param $ show ref]
 
 {- Checks if HEAD exists. It generally will, except for in a repository
  - that was just created. -}
diff --git a/GitAnnex.hs b/GitAnnex.hs
--- a/GitAnnex.hs
+++ b/GitAnnex.hs
@@ -147,11 +147,11 @@
 options = Option.common ++
 	[ Option ['N'] ["numcopies"] (ReqArg setnumcopies paramNumber)
 		"override default number of copies"
-	, Option [] ["trust"] (ReqArg (Remote.forceTrust Trusted) paramRemote)
+	, Option [] ["trust"] (trustArg Trusted)
 		"override trust setting"
-	, Option [] ["semitrust"] (ReqArg (Remote.forceTrust SemiTrusted) paramRemote)
+	, Option [] ["semitrust"] (trustArg SemiTrusted)
 		"override trust setting back to default"
-	, Option [] ["untrust"] (ReqArg (Remote.forceTrust UnTrusted) paramRemote)
+	, Option [] ["untrust"] (trustArg UnTrusted)
 		"override trust setting to untrusted"
 	, Option ['c'] ["config"] (ReqArg setgitconfig "NAME=VALUE")
 		"override git configuration setting"
@@ -173,13 +173,16 @@
 		"skip files smaller than a size"
 	, Option ['T'] ["time-limit"] (ReqArg Limit.addTimeLimit paramTime)
 		"stop after the specified amount of time"
-	, Option [] ["trust-glacier"] (NoArg (Annex.setFlag "trustglacier")) "Trust Amazon Glacier inventory"
+	, Option [] ["trust-glacier"] (NoArg (Annex.setFlag "trustglacier"))
+		"Trust Amazon Glacier inventory"
 	] ++ Option.matcher
   where
 	setnumcopies v = maybe noop
 		(\n -> Annex.changeGitConfig $ \c -> c { annexNumCopies = n })
 		(readish v)
 	setgitconfig v = Annex.changeGitRepo =<< inRepo (Git.Config.store v)
+
+	trustArg t = ReqArg (Remote.forceTrust t) paramRemote
 
 header :: String
 header = "Usage: git-annex command [option ..]"
diff --git a/Limit.hs b/Limit.hs
--- a/Limit.hs
+++ b/Limit.hs
@@ -13,7 +13,8 @@
 import qualified Data.Set as S
 import qualified Data.Map as M
 import System.Path.WildMatch
-import Text.Regex
+import Text.Regex.TDFA
+import Text.Regex.TDFA.String
 
 import Common.Annex
 import qualified Annex
@@ -50,7 +51,8 @@
 		Right r -> return r
 		Left l -> do
 			let matcher = Utility.Matcher.generate (reverse l)
-			Annex.changeState $ \s -> s { Annex.limit = Right matcher }
+			Annex.changeState $ \s ->
+				s { Annex.limit = Right matcher }
 			return matcher
 
 {- Adds something to the limit list, which is built up reversed. -}
@@ -83,12 +85,17 @@
 limitExclude glob = Right $ const $ return . not . matchglob glob
 
 {- Could just use wildCheckCase, but this way the regex is only compiled
- - once. -}
+ - once. Also, we use regex-TDFA because it's less buggy in its support
+ - of non-unicode characters. -}
 matchglob :: String -> Annex.FileInfo -> Bool
 matchglob glob (Annex.FileInfo { Annex.matchFile = f }) =
-	isJust $ matchRegex cregex f
+	case cregex of
+		Right r -> case execute r f of
+			Right (Just _) -> True
+			_ -> False
+		Left _ -> error $ "failed to compile regex: " ++ regex
   where
-	cregex = mkRegex regex
+	cregex = compile defaultCompOpt defaultExecOpt regex
 	regex = '^':wildToRegex glob
 
 {- Adds a limit to skip files not believed to be present
diff --git a/Locations/UserConfig.hs b/Locations/UserConfig.hs
--- a/Locations/UserConfig.hs
+++ b/Locations/UserConfig.hs
@@ -8,6 +8,7 @@
 module Locations.UserConfig where
 
 import Common
+import Utility.TempFile
 import Utility.FreeDesktop
 
 {- ~/.config/git-annex/file -}
@@ -18,6 +19,31 @@
 
 autoStartFile :: IO FilePath
 autoStartFile = userConfigFile "autostart"
+
+{- Returns anything listed in the autostart file (which may not exist). -}
+readAutoStartFile :: IO [FilePath]
+readAutoStartFile = do
+	f <- autoStartFile
+	nub . lines <$> catchDefaultIO "" (readFile f)
+
+{- Adds a directory to the autostart file. -}
+addAutoStartFile :: FilePath -> IO ()
+addAutoStartFile path = do
+	dirs <- readAutoStartFile
+	when (path `notElem` dirs) $ do
+		f <- autoStartFile
+		createDirectoryIfMissing True (parentDir f)
+		viaTmp writeFile f $ unlines $ dirs ++ [path]
+
+{- Removes a directory from the autostart file. -}
+removeAutoStartFile :: FilePath -> IO ()
+removeAutoStartFile path = do
+	dirs <- readAutoStartFile
+	when (path `elem` dirs) $ do
+		f <- autoStartFile
+		createDirectoryIfMissing True (parentDir f)
+		viaTmp writeFile f $ unlines $
+			filter (not . equalFilePath path) dirs
 
 {- The path to git-annex is written here; which is useful when cabal
  - has installed it to some aweful non-PATH location. -}
diff --git a/Logs/Presence.hs b/Logs/Presence.hs
--- a/Logs/Presence.hs
+++ b/Logs/Presence.hs
@@ -32,6 +32,7 @@
 
 import Common.Annex
 import qualified Annex.Branch
+import Utility.QuickCheck
 
 data LogLine = LogLine {
 	date :: POSIXTime,
@@ -74,10 +75,6 @@
 	genstatus InfoPresent = "1"
 	genstatus InfoMissing = "0"
 
--- for quickcheck
-prop_parse_show_log :: [LogLine] -> Bool
-prop_parse_show_log l = parseLog (showLog l) == l
-
 {- Generates a new LogLine with the current date. -}
 logNow :: LogStatus -> String -> Annex LogLine
 logNow s i = do
@@ -113,3 +110,13 @@
 	better = maybe True newer $ M.lookup i m
 	newer l' = date l' <= date l
 	i = info l
+
+instance Arbitrary LogLine where
+	arbitrary = LogLine
+		<$> arbitrary
+		<*> elements [minBound..maxBound]
+		<*> arbitrary `suchThat` ('\n' `notElem`)
+
+prop_parse_show_log :: [LogLine] -> Bool
+prop_parse_show_log l = parseLog (showLog l) == l
+
diff --git a/Logs/Transfer.hs b/Logs/Transfer.hs
--- a/Logs/Transfer.hs
+++ b/Logs/Transfer.hs
@@ -14,6 +14,7 @@
 import Types.Remote
 import Types.Key
 import Utility.Percentage
+import Utility.QuickCheck
 
 import System.Posix.Types
 import Data.Time.Clock
@@ -63,6 +64,13 @@
 readLcDirection "download" = Just Download
 readLcDirection _ = Nothing
 
+describeTransfer :: Transfer -> TransferInfo -> String
+describeTransfer t info = unwords
+	[ show $ transferDirection t
+	, show $ transferUUID t
+	, fromMaybe (key2file $ transferKey t) (associatedFile info)
+	]
+
 {- Transfers that will accomplish the same task. -}
 equivilantTransfer :: Transfer -> Transfer -> Bool
 equivilantTransfer t1 t2
@@ -306,15 +314,6 @@
 		then Just <$> readish =<< headMaybe (drop 1 bits)
 		else pure Nothing -- not failure
 
-{- for quickcheck -}
-prop_read_write_transferinfo :: TransferInfo -> Bool
-prop_read_write_transferinfo info
-	| transferRemote info /= Nothing = True -- remote not stored
-	| transferTid info /= Nothing = True -- tid not stored
-	| otherwise = Just (info { transferPaused = False }) == info'
-  where
-	info' = readTransferInfo (transferPid info) (writeTransferInfo info)
-
 parsePOSIXTime :: String -> Maybe POSIXTime
 parsePOSIXTime s = utcTimeToPOSIXSeconds
 	<$> parseTime defaultTimeLocale "%s%Qs" s
@@ -330,3 +329,23 @@
 	</> "failed"
 	</> showLcDirection direction
 	</> filter (/= '/') (fromUUID u)
+
+instance Arbitrary TransferInfo where
+	arbitrary = TransferInfo
+		<$> arbitrary
+		<*> arbitrary
+		<*> pure Nothing -- cannot generate a ThreadID
+		<*> pure Nothing -- remote not needed
+		<*> arbitrary
+		-- associated file cannot be empty (but can be Nothing)
+		<*> arbitrary `suchThat` (/= Just "")
+		<*> arbitrary
+
+prop_read_write_transferinfo :: TransferInfo -> Bool
+prop_read_write_transferinfo info
+	| transferRemote info /= Nothing = True -- remote not stored
+	| transferTid info /= Nothing = True -- tid not stored
+	| otherwise = Just (info { transferPaused = False }) == info'
+  where
+	info' = readTransferInfo (transferPid info) (writeTransferInfo info)
+
diff --git a/Logs/Unused.hs b/Logs/Unused.hs
--- a/Logs/Unused.hs
+++ b/Logs/Unused.hs
@@ -62,11 +62,12 @@
 unusedSpec :: String -> [Int]
 unusedSpec spec
 	| "-" `isInfixOf` spec = range $ separate (== '-') spec
-	| otherwise = catMaybes [readish spec]
+	| otherwise = maybe badspec (: []) (readish spec)
   where
 	range (a, b) = case (readish a, readish b) of
 		(Just x, Just y) -> [x..y]
-		_ -> []
+		_ -> badspec
+	badspec = error $ "Expected number or range, not \"" ++ spec ++ "\""
 
 {- Start action for unused content. Finds the number in the maps, and
  - calls either of 3 actions, depending on the type of unused file. -}
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -4,24 +4,22 @@
 GHC?=ghc
 GHCMAKE=$(GHC) $(GHCFLAGS) --make
 PREFIX=/usr
-
-build: $(all)
+CABAL?=cabal # set to "runghc Setup.hs" if you lack a cabal program
 
-# We bypass cabal, and only run the main ghc --make command for a
-# fast development built. Note: Does not rebuild C libraries.
-fast: dist/caballog
-	$$(grep 'ghc --make' dist/caballog | head -n 1 | sed 's/ -O / /')
-	ln -sf dist/build/git-annex/git-annex git-annex
+# Am I typing :make in vim? Do a fast build.
+ifdef VIM
+all=fast
+endif
 
-dist/caballog: dist/setup-config
-	cabal configure -f-Production
-	cabal build -v2 | tee $@
+build: build-stamp
+build-stamp: $(all)
+	touch $@
 
 Build/SysConfig.hs: configure.hs Build/TestConfig.hs Build/Configure.hs
-	cabal configure
+	$(CABAL) configure
 
 git-annex: Build/SysConfig.hs
-	cabal build
+	$(CABAL) build
 	ln -sf dist/build/git-annex/git-annex git-annex
 
 git-annex.1: doc/git-annex.mdwn
@@ -52,7 +50,7 @@
 
 # hothasktags chokes on some tempolate haskell etc, so ignore errors
 tags:
-	find . | grep -v /.git/ | grep -v /doc/ | egrep '\.hs$$' | xargs hothasktags > tags 2>&1
+	find . | grep -v /.git/ | grep -v /doc/ | egrep '\.hs$$' | xargs hothasktags > tags 2>/dev/null
 
 # If ikiwiki is available, build static html docs suitable for being
 # shipped in the software package.
@@ -73,7 +71,7 @@
 
 clean:
 	rm -rf tmp dist git-annex $(mans) configure  *.tix .hpc \
-		doc/.ikiwiki html dist tags Build/SysConfig.hs
+		doc/.ikiwiki html dist tags Build/SysConfig.hs build-stamp
 
 sdist: clean $(mans)
 	./Build/make-sdist.sh
@@ -149,17 +147,34 @@
 # Cross compile for Android.
 # Uses https://github.com/neurocyte/ghc-android
 android:
-	cabal configure
+	$(CABAL) configure
 # cabal cannot cross compile with custom build type, so workaround
 	sed -i 's/Build-type: Custom/Build-type: Simple/' git-annex.cabal
-	$$HOME/.ghc/android-14/arm-linux-androideabi-4.7/arm-linux-androideabi/bin/cabal configure -f'Android Assistant -Pairing -Webapp -TestSuite'
+	$$HOME/.ghc/android-14/arm-linux-androideabi-4.7/arm-linux-androideabi/bin/cabal configure -f'Android Assistant -Pairing -Webapp'
 	$(MAKE) git-annex
 	sed -i 's/Build-type: Simple/Build-type: Custom/' git-annex.cabal
 
 androidapp:
 	$(MAKE) android
 	$(MAKE) -C standalone/android
-	mkdir -p tmp
-	cp standalone/android/source/term/bin/Term-debug.apk tmp/git-annex.apk
 
-.PHONY: git-annex tags
+# We bypass cabal, and only run the main ghc --make command for a
+# fast development built. Note: Does not rebuild C libraries.
+fast: dist/caballog
+	@$$(grep 'ghc --make' dist/caballog | head -n 1 | sed -e 's/-package-id [^ ]*//g' -e 's/-hide-all-packages//') -O0
+	@ln -sf dist/build/git-annex/git-annex git-annex
+	@$(MAKE) tags >/dev/null 2>&1
+
+dist/caballog: git-annex.cabal
+	$(CABAL) configure -f"-Production" -O0
+	$(CABAL) build -v2 | tee $@
+
+# Hardcoded command line to make hdevtools start up and work.
+# You will need some memory. It's worth it.
+# Note: Don't include WebDAV or Webapp. TH use bloats memory > 500 mb!
+# TODO should be possible to derive this from caballog.
+hdevtools:
+	hdevtools --stop-server || true
+	hdevtools check git-annex.hs -g -cpp -g -i -g -idist/build/git-annex/git-annex-tmp -g -i. -g -idist/build/autogen -g -Idist/build/autogen -g -Idist/build/git-annex/git-annex-tmp -g -IUtility -g -DWITH_TESTSUITE -g -DWITH_S3 -g -DWITH_ASSISTANT -g -DWITH_INOTIFY -g -DWITH_DBUS -g -DWITH_PAIRING -g -DWITH_XMPP -g -optP-include -g -optPdist/build/autogen/cabal_macros.h -g -odir -g dist/build/git-annex/git-annex-tmp -g -hidir -g dist/build/git-annex/git-annex-tmp -g -stubdir -g dist/build/git-annex/git-annex-tmp -g -threaded -g -Wall -g -XHaskell98
+
+.PHONY: git-annex tags build-stamp
diff --git a/Messages.hs b/Messages.hs
--- a/Messages.hs
+++ b/Messages.hs
@@ -23,6 +23,7 @@
 	showEndResult,
 	showErr,
 	warning,
+	warningIO,
 	fileNotFound,
 	indent,
 	maybeShowJSON,
@@ -157,6 +158,12 @@
 	liftIO $ do
 		hFlush stdout
 		hPutStrLn stderr w
+
+warningIO :: String -> IO ()
+warningIO w = do
+	putStr "\n"
+	hFlush stdout
+	hPutStrLn stderr w
 
 {- Displays a warning one time about a file the user specified not existing. -}
 fileNotFound :: FilePath -> Annex ()
diff --git a/Remote.hs b/Remote.hs
--- a/Remote.hs
+++ b/Remote.hs
@@ -24,6 +24,7 @@
 	remoteMap,
 	uuidDescriptions,
 	byName,
+	byNameWithUUID,
 	byCost,
 	prettyPrintUUIDs,
 	prettyListUUIDs,
@@ -72,16 +73,27 @@
 	| otherwise = n ++ " (" ++ desc ++ ")"
 
 {- When a name is specified, looks up the remote matching that name.
- - (Or it can be a UUID.) Only finds currently configured git remotes. -}
+ - (Or it can be a UUID.) -}
 byName :: Maybe String -> Annex (Maybe Remote)
 byName Nothing = return Nothing
 byName (Just n) = either error Just <$> byName' n
+
+{- Like byName, but the remote must have a configured UUID. -}
+byNameWithUUID :: Maybe String -> Annex (Maybe Remote)
+byNameWithUUID n = do
+	v <- byName n
+	return $ checkuuid <$> v
+  where
+	checkuuid r
+		| uuid r == NoUUID = error $ "cannot determine uuid for " ++ name r
+		| otherwise = r
+
 byName' :: String -> Annex (Either String Remote)
 byName' "" = return $ Left "no remote specified"
 byName' n = handle . filter matching <$> remoteList
   where
 	handle [] = Left $ "there is no available git remote named \"" ++ n ++ "\""
-	handle match = Right $ Prelude.head match
+	handle (match:_) = Right match
 	matching r = n == name r || toUUID n == uuid r
 
 {- Looks up a remote by name (or by UUID, or even by description),
diff --git a/Remote/Bup.hs b/Remote/Bup.hs
--- a/Remote/Bup.hs
+++ b/Remote/Bup.hs
@@ -20,6 +20,7 @@
 import qualified Git.Construct
 import qualified Git.Ref
 import Config
+import Config.Cost
 import Remote.Helper.Ssh
 import Remote.Helper.Special
 import Remote.Helper.Encryptable
@@ -44,7 +45,7 @@
 	bupr <- liftIO $ bup2GitRemote buprepo
 	cst <- remoteCost gc $
 		if bupLocal buprepo
-			then semiCheapRemoteCost
+			then nearlyCheapRemoteCost
 			else expensiveRemoteCost
 	(u', bupr') <- getBupUUID bupr u
 	
@@ -130,7 +131,7 @@
 	sendAnnex k (rollback enck buprepo) $ \src -> do
 		params <- bupSplitParams r buprepo enck []
 		liftIO $ catchBoolIO $
-			encrypt cipher (feedFile src) $ \h ->
+			encrypt (getGpgOpts r) cipher (feedFile src) $ \h ->
 				pipeBup params (Just h) Nothing
 
 retrieve :: BupRepo -> Key -> AssociatedFile -> FilePath -> Annex Bool
@@ -204,8 +205,11 @@
 			r' <- Git.Config.read r
 			let olduuid = Git.Config.get "annex.uuid" "" r'
 			when (olduuid == "") $
-				Git.Command.run "config"
-					[Param "annex.uuid", Param v] r'
+				Git.Command.run
+					[ Param "config"
+					, Param "annex.uuid"
+					, Param v
+					] r'
   where
 	v = fromUUID u
 
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 qualified Git
+import Config.Cost
 import Config
 import Utility.FileMode
 import Remote.Helper.Special
@@ -38,7 +39,7 @@
 	cst <- remoteCost gc cheapRemoteCost
 	let chunksize = chunkSize c
 	return $ encryptableRemote c
-		(storeEncrypted dir chunksize)
+		(storeEncrypted dir (getGpgOpts gc) chunksize)
 		(retrieveEncrypted dir chunksize)
 		Remote {
 			uuid = u,
@@ -124,11 +125,11 @@
 					storeSplit meterupdate chunksize dests
 						=<< L.readFile src
 
-storeEncrypted :: FilePath -> ChunkSize -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted d chunksize (cipher, enck) k p = sendAnnex k (void $ remove d enck) $ \src -> 
+storeEncrypted :: FilePath -> GpgOpts -> ChunkSize -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
+storeEncrypted d gpgOpts chunksize (cipher, enck) k p = sendAnnex k (void $ remove d enck) $ \src ->
 	metered (Just p) k $ \meterupdate ->
 		storeHelper d chunksize enck $ \dests ->
-			encrypt cipher (feedFile src) $ readBytes $ \b ->
+			encrypt gpgOpts cipher (feedFile src) $ readBytes $ \b ->
 				case chunksize of
 					Nothing -> do
 						let dest = Prelude.head dests
diff --git a/Remote/Git.hs b/Remote/Git.hs
--- a/Remote/Git.hs
+++ b/Remote/Git.hs
@@ -35,6 +35,7 @@
 import qualified Utility.Url as Url
 import Utility.TempFile
 import Config
+import Config.Cost
 import Init
 import Types.Key
 import qualified Fields
@@ -141,10 +142,10 @@
 				{- Is this remote just not available, or does
 				 - it not have git-annex-shell?
 				 - Find out by trying to fetch from the remote. -}
-				whenM (inRepo $ Git.Command.runBool "fetch" [Param "--quiet", Param n]) $ do
+				whenM (inRepo $ Git.Command.runBool [Param "fetch", Param "--quiet", Param n]) $ do
 					let k = "remote." ++ n ++ ".annex-ignore"
 					warning $ "Remote " ++ n ++ " does not have git-annex installed; setting " ++ k
-					inRepo $ Git.Command.run "config" [Param k, Param "true"]
+					inRepo $ Git.Command.run [Param "config", Param k, Param "true"]
 				return r
 			_ -> return r
 	| Git.repoIsHttp r = do
@@ -161,8 +162,15 @@
 			=<< liftIO (try a :: IO (Either SomeException Git.Repo))
 
 	pipedconfig cmd params =
-		withHandle StdoutHandle createProcessSuccess p $
-			Git.Config.hRead r
+		withHandle StdoutHandle createProcessSuccess p $ \h -> do
+ 			fileEncoding h
+			val <- hGetContentsStrict h
+			r' <- Git.Config.store val r
+			when (getUncachedUUID r' == NoUUID && not (null val)) $ do
+				warningIO $ "Failed to get annex.uuid configuration of repository " ++ Git.repoDescribe r
+				warningIO $ "Instead, got: " ++ show val
+				warningIO $ "This is unexpected; please check the network transport!"
+			return r'
 	  where
 		p = proc cmd $ toCommand params
 
@@ -344,16 +352,20 @@
   where
 	copylocal Nothing = return False
 	copylocal (Just (object, checksuccess)) = do
+		-- The checksuccess action is going to be run in
+		-- the remote's Annex, but it needs access to the current
+		-- Annex monad's state.
+		checksuccessio <- Annex.withCurrentState checksuccess
 		let params = rsyncParams r
 		u <- getUUID
 		-- run copy from perspective of remote
 		liftIO $ onLocal (repo r) $ ifM (Annex.Content.inAnnex key)
-			( return False
+			( return True
 			, do
 				ensureInitialized
 				download u key file noRetry $
 					Annex.Content.saveState True `after`
-						Annex.Content.getViaTmpChecked checksuccess key
+						Annex.Content.getViaTmpChecked (liftIO checksuccessio) key
 							(\d -> rsyncOrCopyFile params object d p)
 			)
 
diff --git a/Remote/Glacier.hs b/Remote/Glacier.hs
--- a/Remote/Glacier.hs
+++ b/Remote/Glacier.hs
@@ -16,6 +16,7 @@
 import Types.Key
 import qualified Git
 import Config
+import Config.Cost
 import Remote.Helper.Special
 import Remote.Helper.Encryptable
 import qualified Remote.Helper.AWS as AWS
@@ -93,7 +94,7 @@
 storeEncrypted r (cipher, enck) k m = sendAnnex k (void $ remove r enck) $ \src -> do
 	metered (Just m) k $ \meterupdate ->
 		storeHelper r enck $ \h ->
-			encrypt cipher (feedFile src)
+			encrypt (getGpgOpts r) cipher (feedFile src)
 				(readBytes $ meteredWrite meterupdate h)
 
 retrieve :: Remote -> Key -> AssociatedFile -> FilePath -> Annex Bool
diff --git a/Remote/Helper/Encryptable.hs b/Remote/Helper/Encryptable.hs
--- a/Remote/Helper/Encryptable.hs
+++ b/Remote/Helper/Encryptable.hs
@@ -13,7 +13,7 @@
 import Types.Remote
 import Crypto
 import qualified Annex
-import Config
+import Config.Cost
 import Utility.Base64
 
 {- Encryption setup for a remote. The user must specify whether to use
diff --git a/Remote/Helper/Special.hs b/Remote/Helper/Special.hs
--- a/Remote/Helper/Special.hs
+++ b/Remote/Helper/Special.hs
@@ -34,7 +34,7 @@
 	set ("annex-"++k) v
 	set ("annex-uuid") (fromUUID u)
   where
-	set a b = inRepo $ Git.Command.run "config"
-		[Param (configsetting a), Param b]
+	set a b = inRepo $ Git.Command.run
+		[Param "config", Param (configsetting a), Param b]
 	remotename = fromJust (M.lookup "name" c)
 	configsetting s = "remote." ++ remotename ++ "." ++ s
diff --git a/Remote/Hook.hs b/Remote/Hook.hs
--- a/Remote/Hook.hs
+++ b/Remote/Hook.hs
@@ -16,6 +16,7 @@
 import Types.Key
 import qualified Git
 import Config
+import Config.Cost
 import Annex.Content
 import Remote.Helper.Special
 import Remote.Helper.Encryptable
@@ -33,7 +34,7 @@
 gen r u c gc = do
 	cst <- remoteCost gc expensiveRemoteCost
 	return $ encryptableRemote c
-		(storeEncrypted hooktype)
+		(storeEncrypted hooktype $ getGpgOpts gc)
 		(retrieveEncrypted hooktype)
 		Remote {
 			uuid = u,
@@ -106,10 +107,10 @@
 store h k _f _p = sendAnnex k (void $ remove h k) $ \src ->
 	runHook h "store" k (Just src) $ return True
 
-storeEncrypted :: String -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted h (cipher, enck) k _p = withTmp enck $ \tmp ->
+storeEncrypted :: String -> GpgOpts -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
+storeEncrypted h gpgOpts (cipher, enck) k _p = withTmp enck $ \tmp ->
 	sendAnnex k (void $ remove h enck) $ \src -> do
-		liftIO $ encrypt cipher (feedFile src) $
+		liftIO $ encrypt gpgOpts cipher (feedFile src) $
 			readBytes $ L.writeFile tmp
 		runHook h "store" enck (Just tmp) $ return True
 
diff --git a/Remote/Rsync.hs b/Remote/Rsync.hs
--- a/Remote/Rsync.hs
+++ b/Remote/Rsync.hs
@@ -15,6 +15,7 @@
 import Types.Remote
 import qualified Git
 import Config
+import Config.Cost
 import Annex.Content
 import Remote.Helper.Special
 import Remote.Helper.Encryptable
@@ -43,7 +44,7 @@
 gen r u c gc = do
 	cst <- remoteCost gc expensiveRemoteCost
 	return $ encryptableRemote c
-		(storeEncrypted o)
+		(storeEncrypted o $ getGpgOpts gc)
 		(retrieveEncrypted o)
 		Remote
 			{ uuid = u
@@ -104,10 +105,10 @@
 store :: RsyncOpts -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
 store o k _f p = sendAnnex k (void $ remove o k) $ rsyncSend o p k False
 
-storeEncrypted :: RsyncOpts -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
-storeEncrypted o (cipher, enck) k p = withTmp enck $ \tmp ->
+storeEncrypted :: RsyncOpts -> GpgOpts -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
+storeEncrypted o gpgOpts (cipher, enck) k p = withTmp enck $ \tmp ->
 	sendAnnex k (void $ remove o enck) $ \src -> do
-		liftIO $ encrypt cipher (feedFile src) $
+		liftIO $ encrypt gpgOpts cipher (feedFile src) $
 			readBytes $ L.writeFile tmp
 		rsyncSend o p enck True tmp
 
diff --git a/Remote/S3.hs b/Remote/S3.hs
--- a/Remote/S3.hs
+++ b/Remote/S3.hs
@@ -21,6 +21,7 @@
 import Types.Key
 import qualified Git
 import Config
+import Config.Cost
 import Remote.Helper.Special
 import Remote.Helper.Encryptable
 import qualified Remote.Helper.AWS as AWS
@@ -122,7 +123,7 @@
 	-- To get file size of the encrypted content, have to use a temp file.
 	-- (An alternative would be chunking to to a constant size.)
 	withTmp enck $ \tmp -> sendAnnex k (void $ remove r enck) $ \src -> do
-		liftIO $ encrypt cipher (feedFile src) $
+		liftIO $ encrypt (getGpgOpts r) cipher (feedFile src) $
 			readBytes $ L.writeFile tmp
 		res <- storeHelper (conn, bucket) r enck p tmp
 		s3Bool res
diff --git a/Remote/Web.hs b/Remote/Web.hs
--- a/Remote/Web.hs
+++ b/Remote/Web.hs
@@ -13,6 +13,7 @@
 import qualified Git.Construct
 import Annex.Content
 import Config
+import Config.Cost
 import Logs.Web
 import qualified Utility.Url as Url
 import Types.Key
diff --git a/Remote/WebDAV.hs b/Remote/WebDAV.hs
--- a/Remote/WebDAV.hs
+++ b/Remote/WebDAV.hs
@@ -7,12 +7,6 @@
 
 {-# LANGUAGE ScopedTypeVariables, CPP #-}
 
-#if defined VERSION_http_conduit
-#if ! MIN_VERSION_http_conduit(1,9,0)
-#define WITH_OLD_HTTP_CONDUIT
-#endif
-#endif
-
 module Remote.WebDAV (remote, davCreds, setCredsEnv) where
 
 import Network.Protocol.HTTP.DAV
@@ -30,6 +24,7 @@
 import Types.Remote
 import qualified Git
 import Config
+import Config.Cost
 import Remote.Helper.Special
 import Remote.Helper.Encryptable
 import Remote.Helper.Chunked
@@ -98,7 +93,8 @@
 storeEncrypted r (cipher, enck) k p = metered (Just p) k $ \meterupdate ->
 	davAction r False $ \(baseurl, user, pass) ->
 		sendAnnex k (void $ remove r enck) $ \src ->
-			liftIO $ encrypt cipher (streamMeteredFile src meterupdate) $
+			liftIO $ encrypt (getGpgOpts r) cipher
+				(streamMeteredFile src meterupdate) $
 				readBytes $ storeHelper r enck baseurl user pass
 
 storeHelper :: Remote -> Key -> DavUrl -> DavUser -> DavPass -> L.ByteString -> IO Bool
@@ -234,7 +230,7 @@
 davUrlExists url user pass = decode <$> catchHttp (getProps url user pass)
   where
 	decode (Right _) = Right True
-#ifdef WITH_OLD_HTTP_CONDUIT
+#if ! MIN_VERSION_http_conduit(1,9,0)
 	decode (Left (Left (StatusCodeException status _)))
 #else
 	decode (Left (Left (StatusCodeException status _ _)))
@@ -285,7 +281,7 @@
 type EitherException = Either HttpException E.IOException
 
 showEitherException :: EitherException -> String
-#ifdef WITH_OLD_HTTP_CONDUIT
+#if ! MIN_VERSION_http_conduit(1,9,0)
 showEitherException (Left (StatusCodeException status _)) =
 #else
 showEitherException (Left (StatusCodeException status _ _)) =
diff --git a/Test.hs b/Test.hs
--- a/Test.hs
+++ b/Test.hs
@@ -5,14 +5,11 @@
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-
 module Test where
 
 import Test.HUnit
-import Test.HUnit.Tools
 import Test.QuickCheck
-import Test.QuickCheck.Instances ()
+import Test.QuickCheck.Test
 
 import System.Posix.Directory (changeWorkingDirectory)
 import System.Posix.Files
@@ -20,10 +17,9 @@
 import Control.Exception.Extensible
 import qualified Data.Map as M
 import System.IO.HVFS (SystemFS(..))
-import Text.JSON
+import qualified Text.JSON
 
 import Common
-import Utility.QuickCheck ()
 
 import qualified Utility.SafeCommand
 import qualified Annex
@@ -46,7 +42,7 @@
 import qualified Remote
 import qualified Types.Key
 import qualified Types.Messages
-import qualified Config
+import qualified Config.Cost
 import qualified Crypto
 import qualified Utility.Path
 import qualified Utility.FileMode
@@ -58,110 +54,110 @@
 import qualified Utility.Misc
 import qualified Utility.InodeCache
 
--- instances for quickcheck
-instance Arbitrary Types.Key.Key where
-	arbitrary = Types.Key.Key
-		<$> arbitrary
-		<*> (listOf1 $ elements ['A'..'Z']) -- BACKEND
-		<*> ((abs <$>) <$> arbitrary) -- size cannot be negative
-		<*> arbitrary
-
-instance Arbitrary Logs.Transfer.TransferInfo where
-	arbitrary = Logs.Transfer.TransferInfo
-		<$> arbitrary
-		<*> arbitrary
-		<*> pure Nothing -- cannot generate a ThreadID
-		<*> pure Nothing -- remote not needed
-		<*> arbitrary
-		-- associated file cannot be empty (but can be Nothing)
-		<*> arbitrary `suchThat` (/= Just "")
-		<*> arbitrary
-
-instance Arbitrary Utility.InodeCache.InodeCache where
-	arbitrary = Utility.InodeCache.InodeCache
-		<$> arbitrary
-		<*> arbitrary
-		<*> arbitrary
-
-instance Arbitrary Logs.Presence.LogLine where
-	arbitrary = Logs.Presence.LogLine
-		<$> arbitrary
-		<*> elements [minBound..maxBound]
-		<*> arbitrary `suchThat` ('\n' `notElem`)
-
 main :: IO ()
 main = do
+	divider
+	putStrLn "First, some automated quick checks of properties ..."
+	divider
+	qcok <- all isSuccess <$> sequence quickcheck
+	divider
+	putStrLn "Now, some broader checks ..."
+	putStrLn "  (Do not be alarmed by odd output here; it's normal."
+        putStrLn "   wait for the last line to see how it went.)"
 	prepare
-	r <- runVerboseTests $ TestList [quickcheck, blackbox]
+	rs <- forM hunit $ \t -> do
+		divider
+		t
 	cleanup tmpdir
-	propigate r
+	divider
+	propigate rs qcok
+  where
+	divider = putStrLn $ take 70 $ repeat '-'
 
-propigate :: (Counts, Int) -> IO ()
-propigate (Counts { errors = e , failures = f }, _)
-	| e+f > 0 = error "failed"
-	| otherwise = return ()
+propigate :: [Counts] -> Bool -> IO ()
+propigate cs qcok
+	| countsok && qcok = putStrLn "All tests ok."
+	| otherwise = do
+		unless qcok $
+			putStrLn "Quick check tests failed! This is a bug in git-annex."
+		unless countsok $ do
+			putStrLn "Some tests failed!"
+			putStrLn "  (This could be due to a bug in git-annex, or an incompatability"
+			putStrLn "   with utilities, such as git, installed on this system.)"
+		exitFailure
+  where
+	noerrors (Counts { errors = e , failures = f }) = e + f == 0
+	countsok = all noerrors cs
 
-quickcheck :: Test
-quickcheck = TestLabel "quickcheck" $ TestList
-	[ qctest "prop_idempotent_deencode_git" Git.Filename.prop_idempotent_deencode
-	, qctest "prop_idempotent_deencode" Utility.Format.prop_idempotent_deencode
-	, qctest "prop_idempotent_fileKey" Locations.prop_idempotent_fileKey
-	, qctest "prop_idempotent_key_encode" Types.Key.prop_idempotent_key_encode
-	, qctest "prop_idempotent_shellEscape" Utility.SafeCommand.prop_idempotent_shellEscape
-	, qctest "prop_idempotent_shellEscape_multiword" Utility.SafeCommand.prop_idempotent_shellEscape_multiword
-	, qctest "prop_idempotent_configEscape" Logs.Remote.prop_idempotent_configEscape
-	, qctest "prop_parse_show_Config" Logs.Remote.prop_parse_show_Config
-	, qctest "prop_parentDir_basics" Utility.Path.prop_parentDir_basics
-	, qctest "prop_relPathDirToFile_basics" Utility.Path.prop_relPathDirToFile_basics
-	, qctest "prop_relPathDirToFile_regressionTest" Utility.Path.prop_relPathDirToFile_regressionTest
-	, qctest "prop_cost_sane" Config.prop_cost_sane
-	, qctest "prop_hmacWithCipher_sane" Crypto.prop_hmacWithCipher_sane
-	, qctest "prop_TimeStamp_sane" Logs.UUIDBased.prop_TimeStamp_sane
-	, qctest "prop_addLog_sane" Logs.UUIDBased.prop_addLog_sane
-	, qctest "prop_verifiable_sane" Utility.Verifiable.prop_verifiable_sane
-	, qctest "prop_segment_regressionTest" Utility.Misc.prop_segment_regressionTest
-	, qctest "prop_read_write_transferinfo" Logs.Transfer.prop_read_write_transferinfo
-	, qctest "prop_read_show_inodecache" Utility.InodeCache.prop_read_show_inodecache
-	, qctest "prop_parse_show_log" Logs.Presence.prop_parse_show_log
-	, qctest "prop_read_show_TrustLevel" Types.TrustLevel.prop_read_show_TrustLevel
-	, qctest "prop_parse_show_TrustLog" Logs.Trust.prop_parse_show_TrustLog
+quickcheck :: [IO Result]
+quickcheck =
+	[ check "prop_idempotent_deencode_git" Git.Filename.prop_idempotent_deencode
+	, check "prop_idempotent_deencode" Utility.Format.prop_idempotent_deencode
+	, check "prop_idempotent_fileKey" Locations.prop_idempotent_fileKey
+	, check "prop_idempotent_key_encode" Types.Key.prop_idempotent_key_encode
+	, check "prop_idempotent_shellEscape" Utility.SafeCommand.prop_idempotent_shellEscape
+	, check "prop_idempotent_shellEscape_multiword" Utility.SafeCommand.prop_idempotent_shellEscape_multiword
+	, check "prop_idempotent_configEscape" Logs.Remote.prop_idempotent_configEscape
+	, check "prop_parse_show_Config" Logs.Remote.prop_parse_show_Config
+	, check "prop_parentDir_basics" Utility.Path.prop_parentDir_basics
+	, check "prop_relPathDirToFile_basics" Utility.Path.prop_relPathDirToFile_basics
+	, check "prop_relPathDirToFile_regressionTest" Utility.Path.prop_relPathDirToFile_regressionTest
+	, check "prop_cost_sane" Config.Cost.prop_cost_sane
+	, check "prop_hmacWithCipher_sane" Crypto.prop_hmacWithCipher_sane
+	, check "prop_TimeStamp_sane" Logs.UUIDBased.prop_TimeStamp_sane
+	, check "prop_addLog_sane" Logs.UUIDBased.prop_addLog_sane
+	, check "prop_verifiable_sane" Utility.Verifiable.prop_verifiable_sane
+	, check "prop_segment_regressionTest" Utility.Misc.prop_segment_regressionTest
+	, check "prop_read_write_transferinfo" Logs.Transfer.prop_read_write_transferinfo
+	, check "prop_read_show_inodecache" Utility.InodeCache.prop_read_show_inodecache
+	, check "prop_parse_show_log" Logs.Presence.prop_parse_show_log
+	, check "prop_read_show_TrustLevel" Types.TrustLevel.prop_read_show_TrustLevel
+	, check "prop_parse_show_TrustLog" Logs.Trust.prop_parse_show_TrustLog
 	]
+  where
+	check desc prop = do
+		putStrLn desc
+		quickCheckResult prop
 
-blackbox :: Test
-blackbox = TestLabel "blackbox" $ TestList
+hunit :: [IO Counts]
+hunit =
 	-- test order matters, later tests may rely on state from earlier
-	[ test_init
-	, test_add
-	, test_reinject
-	, test_unannex
-	, test_drop
-	, test_get
-	, test_move
-	, test_copy
-	, test_lock
-	, test_edit
-	, test_fix
-	, test_trust
-	, test_fsck
-	, test_migrate
-	, test_unused
-	, test_describe
-	, test_find
-	, test_merge
-	, test_status
-	, test_version
-	, test_sync
-	, test_sync_regression
-	, test_map
-	, test_uninit
-	, test_upgrade
-	, test_whereis
-	, test_hook_remote
-	, test_directory_remote
-	, test_rsync_remote
-	, test_bup_remote
-	, test_crypto
+	[ check "init" test_init
+	, check "add" test_add
+	, check "reinject" test_reinject
+	, check "unannex" test_unannex
+	, check "drop" test_drop
+	, check "get" test_get
+	, check "move" test_move
+	, check "copy" test_copy
+	, check "lock" test_lock
+	, check "edit" test_edit
+	, check "fix" test_fix
+	, check "trust" test_trust
+	, check "fsck" test_fsck
+	, check "migrate" test_migrate
+	, check" unused" test_unused
+	, check "describe" test_describe
+	, check "find" test_find
+	, check "merge" test_merge
+	, check "status" test_status
+	, check "version" test_version
+	, check "sync" test_sync
+	, check "sync regression" test_sync_regression
+	, check "map" test_map
+	, check "uninit" test_uninit
+	, check "upgrade" test_upgrade
+	, check "whereis" test_whereis
+	, check "hook remote" test_hook_remote
+	, check "directory remote" test_directory_remote
+	, check "rsync remote" test_rsync_remote
+	, check "bup remote" test_bup_remote
+	, check "crypto" test_crypto
 	]
+  where
+	check desc t = do
+		putStrLn desc
+		runTestTT t
 
 test_init :: Test
 test_init = "git-annex init" ~: TestCase $ innewrepo $ do
@@ -577,9 +573,9 @@
 test_status :: Test
 test_status = "git-annex status" ~: intmpclonerepo $ do
 	json <- git_annex_output "status" ["--json"]
-	case Text.JSON.decodeStrict json :: Text.JSON.Result (JSObject JSValue) of
-		Ok _ -> return ()
-		Error e -> assertFailure e
+	case Text.JSON.decodeStrict json :: Text.JSON.Result (Text.JSON.JSObject Text.JSON.JSValue) of
+		Text.JSON.Ok _ -> return ()
+		Text.JSON.Error e -> assertFailure e
 
 test_version :: Test
 test_version = "git-annex version" ~: intmpclonerepo $ do
diff --git a/Types/GitConfig.hs b/Types/GitConfig.hs
--- a/Types/GitConfig.hs
+++ b/Types/GitConfig.hs
@@ -16,6 +16,7 @@
 import qualified Git
 import qualified Git.Config
 import Utility.DataUnits
+import Config.Cost
 
 {- Main git-annex settings. Each setting corresponds to a git-config key
  - such as annex.foo -}
@@ -77,7 +78,7 @@
  - key such as <remote>.annex-foo, or if that is not set, a default from
  - annex.foo -}
 data RemoteGitConfig = RemoteGitConfig
-	{ remoteAnnexCost :: Maybe Int
+	{ remoteAnnexCost :: Maybe Cost
 	, remoteAnnexCostCommand :: Maybe String
 	, remoteAnnexIgnore :: Bool
 	, remoteAnnexSync :: Bool
@@ -88,6 +89,7 @@
 	-- these settings are specific to particular types of remotes
 	, remoteAnnexSshOptions :: [String]
 	, remoteAnnexRsyncOptions :: [String]
+	, remoteAnnexGnupgOptions :: [String]
 	, remoteAnnexRsyncUrl :: Maybe String
 	, remoteAnnexBupRepo :: Maybe String
 	, remoteAnnexBupSplitOptions :: [String]
@@ -107,6 +109,7 @@
 
 	, remoteAnnexSshOptions = getoptions "ssh-options"
 	, remoteAnnexRsyncOptions = getoptions "rsync-options"
+	, remoteAnnexGnupgOptions = getoptions "gnupg-options"
 	, remoteAnnexRsyncUrl = notempty $ getmaybe "rsyncurl"
 	, remoteAnnexBupRepo = getmaybe "buprepo"
 	, remoteAnnexBupSplitOptions = getoptions "bup-split-options"
diff --git a/Types/Key.hs b/Types/Key.hs
--- a/Types/Key.hs
+++ b/Types/Key.hs
@@ -19,6 +19,7 @@
 import System.Posix.Types
 
 import Common
+import Utility.QuickCheck
 
 {- A Key has a unique name, is associated with a key/value backend,
  - and may contain other optional metadata. -}
@@ -73,6 +74,13 @@
 	addfield 's' k v = Just k { keySize = readish v }
 	addfield 'm' k v = Just k { keyMtime = readish v }
 	addfield _ _ _ = Nothing
+
+instance Arbitrary Key where
+	arbitrary = Key
+		<$> arbitrary
+		<*> (listOf1 $ elements ['A'..'Z']) -- BACKEND
+		<*> ((abs <$>) <$> arbitrary) -- size cannot be negative
+		<*> arbitrary
 
 prop_idempotent_key_encode :: Key -> Bool
 prop_idempotent_key_encode k = Just k == (file2key . key2file) k
diff --git a/Types/Remote.hs b/Types/Remote.hs
--- a/Types/Remote.hs
+++ b/Types/Remote.hs
@@ -17,6 +17,7 @@
 import Types.UUID
 import Types.Meters
 import Types.GitConfig
+import Config.Cost
 
 type RemoteConfigKey = String
 type RemoteConfig = M.Map RemoteConfigKey String
@@ -46,7 +47,7 @@
 	-- each Remote has a human visible name
 	name :: String,
 	-- Remotes have a use cost; higher is more expensive
-	cost :: Int,
+	cost :: Cost,
 	-- Transfers a key to the remote.
 	storeKey :: Key -> AssociatedFile -> MeterUpdate -> a Bool,
 	-- retrieves a key's contents to a file
diff --git a/Upgrade/V2.hs b/Upgrade/V2.hs
--- a/Upgrade/V2.hs
+++ b/Upgrade/V2.hs
@@ -54,7 +54,7 @@
 	showProgress
 
 	when e $ do
-		inRepo $ Git.Command.run "rm" [Param "-r", Param "-f", Param "-q", File old]
+		inRepo $ Git.Command.run [Param "rm", Param "-r", Param "-f", Param "-q", File old]
 		unless bare $ inRepo gitAttributesUnWrite
 	showProgress
 
@@ -105,8 +105,8 @@
 			Annex.Branch.update -- just in case
 			showAction "pushing new git-annex branch to origin"
 			showOutput
-			inRepo $ Git.Command.run "push"
-				[Param "origin", Param $ show Annex.Branch.name]
+			inRepo $ Git.Command.run
+				[Param "push", Param "origin", Param $ show Annex.Branch.name]
 		_ -> do
 			-- no origin exists, so just let the user
 			-- know about the new branch
@@ -129,7 +129,7 @@
 		c <- readFileStrict attributes
 		liftIO $ viaTmp writeFile attributes $ unlines $
 			filter (`notElem` attrLines) $ lines c
-		Git.Command.run "add" [File attributes] repo
+		Git.Command.run [Param "add", File attributes] repo
 
 stateDir :: FilePath
 stateDir = addTrailingPathSeparator ".git-annex"
diff --git a/Utility/Base64.hs b/Utility/Base64.hs
--- a/Utility/Base64.hs
+++ b/Utility/Base64.hs
@@ -5,14 +5,20 @@
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
-module Utility.Base64 (toB64, fromB64) where
+module Utility.Base64 (toB64, fromB64Maybe, fromB64) where
 
 import Codec.Binary.Base64
 import Data.Bits.Utils
+import Control.Applicative
+import Data.Maybe
 
 toB64 :: String -> String		
 toB64 = encode . s2w8
 
+fromB64Maybe :: String -> Maybe String
+fromB64Maybe s = w82s <$> decode s
+
 fromB64 :: String -> String
-fromB64 s = maybe bad w82s $ decode s
-  where bad = error "bad base64 encoded data"
+fromB64 = fromMaybe bad . fromB64Maybe
+  where
+	bad = error "bad base64 encoded data"
diff --git a/Utility/Daemon.hs b/Utility/Daemon.hs
--- a/Utility/Daemon.hs
+++ b/Utility/Daemon.hs
@@ -8,6 +8,7 @@
 module Utility.Daemon where
 
 import Common
+import Utility.LogFile
 
 import System.Posix
 
@@ -39,16 +40,6 @@
 		a
 		out
 	out = exitImmediately ExitSuccess
-
-redirLog :: Fd -> IO ()
-redirLog logfd = do
-	mapM_ (redir logfd) [stdOutput, stdError]
-	closeFd logfd
-
-redir :: Fd -> Fd -> IO ()
-redir newh h = do
-	closeFd h
-	void $ dupTo newh h
 
 {- Locks the pid file, with an exclusive, non-blocking lock.
  - Writes the pid to the file, fully atomically.
diff --git a/Utility/DirWatcher.hs b/Utility/DirWatcher.hs
--- a/Utility/DirWatcher.hs
+++ b/Utility/DirWatcher.hs
@@ -13,7 +13,7 @@
 
 module Utility.DirWatcher where
 
-import Utility.Types.DirWatcher
+import Utility.DirWatcher.Types
 
 #if WITH_INOTIFY
 import qualified Utility.INotify as INotify
diff --git a/Utility/DirWatcher/Types.hs b/Utility/DirWatcher/Types.hs
new file mode 100644
--- /dev/null
+++ b/Utility/DirWatcher/Types.hs
@@ -0,0 +1,24 @@
+{- generic directory watching types
+ -
+ - Copyright 2012 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Utility.DirWatcher.Types where
+
+import Common
+
+type Hook a = Maybe (a -> Maybe FileStatus -> IO ())
+
+data WatchHooks = WatchHooks
+	{ addHook :: Hook FilePath
+	, addSymlinkHook :: Hook FilePath
+	, delHook :: Hook FilePath
+	, delDirHook :: Hook FilePath
+	, errHook :: Hook String -- error message
+	, modifyHook :: Hook FilePath
+	}
+
+mkWatchHooks :: WatchHooks
+mkWatchHooks = WatchHooks Nothing Nothing Nothing Nothing Nothing Nothing
diff --git a/Utility/DiskFree.hs b/Utility/DiskFree.hs
--- a/Utility/DiskFree.hs
+++ b/Utility/DiskFree.hs
@@ -5,10 +5,12 @@
  - Licensed under the GNU GPL version 3 or higher.
  -}
 
-{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE ForeignFunctionInterface, CPP #-}
 
 module Utility.DiskFree ( getDiskFree ) where
 
+#ifdef WITH_CLIBS
+
 import Common
 
 import Foreign.C.Types
@@ -27,3 +29,10 @@
 		)
   where
 	safeErrno (Errno v) = v == 0
+
+#else
+
+getDiskFree :: FilePath -> IO (Maybe Integer)
+getDiskFree _ = return Nothing
+
+#endif
diff --git a/Utility/FSEvents.hs b/Utility/FSEvents.hs
--- a/Utility/FSEvents.hs
+++ b/Utility/FSEvents.hs
@@ -8,7 +8,7 @@
 module Utility.FSEvents where
 
 import Common hiding (isDirectory)
-import Utility.Types.DirWatcher
+import Utility.DirWatcher.Types
 
 import System.OSX.FSEvents
 import qualified System.Posix.Files as Files
diff --git a/Utility/Gpg.hs b/Utility/Gpg.hs
--- a/Utility/Gpg.hs
+++ b/Utility/Gpg.hs
@@ -98,14 +98,31 @@
  - It is armored, to avoid newlines, since gpg only reads ciphers up to the
  - first newline. -}
 genRandom :: Int -> IO String
-genRandom size = readStrict
-	[ Params "--gen-random --armor"
+genRandom size = checksize <$> readStrict
+	[ Params params
 	, Param $ show randomquality
 	, Param $ show size
 	]
   where
+	params = "--gen-random --armor"
+
 	-- 1 is /dev/urandom; 2 is /dev/random
 	randomquality = 1 :: Int
+
+ 	{- The size is the number of bytes of entropy desired; the data is
+	 - base64 encoded, so needs 8 bits to represent every 6 bytes of
+	 - entropy. -}
+	expectedlength = size * 8 `div` 6
+
+	checksize s = let len = length s in
+		if len >= expectedlength
+			then s
+			else shortread len
+
+	shortread got = error $ unwords
+		[ "Not enough bytes returned from gpg", params
+		, "(got", show got, "; expected", show expectedlength, ")"
+		]
 
 {- A test key. This is provided pre-generated since generating a new gpg
  - key is too much work (requires too much entropy) for a test suite to
diff --git a/Utility/Gpg/Types.hs b/Utility/Gpg/Types.hs
new file mode 100644
--- /dev/null
+++ b/Utility/Gpg/Types.hs
@@ -0,0 +1,30 @@
+{- gpg data types
+ -
+ - Copyright 2013 guilhem <guilhem@fripost.org>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+module Utility.Gpg.Types where
+
+import Utility.SafeCommand
+import Types.GitConfig
+import Types.Remote
+
+{- GnuPG options. -}
+type GpgOpt = String
+newtype GpgOpts = GpgOpts [GpgOpt]
+
+toParams :: GpgOpts -> [CommandParam]
+toParams (GpgOpts opts) = map Param opts
+
+class LensGpgOpts a where
+	getGpgOpts :: a -> GpgOpts
+
+{- Extract the GnuPG options from a Remote Git Config. -}
+instance LensGpgOpts RemoteGitConfig where
+	getGpgOpts = GpgOpts . remoteAnnexGnupgOptions
+
+{- Extract the GnuPG options from a Remote. -}
+instance LensGpgOpts (RemoteA a) where
+	getGpgOpts = getGpgOpts . gitconfig
diff --git a/Utility/INotify.hs b/Utility/INotify.hs
--- a/Utility/INotify.hs
+++ b/Utility/INotify.hs
@@ -9,7 +9,7 @@
 
 import Common hiding (isDirectory)
 import Utility.ThreadLock
-import Utility.Types.DirWatcher
+import Utility.DirWatcher.Types
 
 import System.INotify
 import qualified System.Posix.Files as Files
diff --git a/Utility/InodeCache.hs b/Utility/InodeCache.hs
--- a/Utility/InodeCache.hs
+++ b/Utility/InodeCache.hs
@@ -9,19 +9,48 @@
 
 import Common
 import System.Posix.Types
+import Utility.QuickCheck
 
-data InodeCache = InodeCache FileID FileOffset EpochTime
-	deriving (Eq, Show)
+data InodeCachePrim = InodeCachePrim FileID FileOffset EpochTime
+	deriving (Show, Eq, Ord)
 
-{- Weak comparison of the inode caches, comparing the size and mtime, but
- - not the actual inode.  Useful when inodes have changed, perhaps
+newtype InodeCache = InodeCache InodeCachePrim
+	deriving (Show)
+
+{- Inode caches can be compared in two different ways, either weakly
+ - or strongly. -}
+data InodeComparisonType = Weakly | Strongly
+	deriving (Eq, Ord)
+
+{- Strong comparison, including inodes. -}
+compareStrong :: InodeCache -> InodeCache -> Bool
+compareStrong (InodeCache x) (InodeCache y) = x == y
+
+{- Weak comparison of the inode caches, comparing the size and mtime,
+ - but not the actual inode.  Useful when inodes have changed, perhaps
  - due to some filesystems being remounted. -}
 compareWeak :: InodeCache -> InodeCache -> Bool
-compareWeak (InodeCache _ size1 mtime1) (InodeCache _ size2 mtime2) =
+compareWeak (InodeCache (InodeCachePrim _ size1 mtime1)) (InodeCache (InodeCachePrim _ size2 mtime2)) =
 	size1 == size2 && mtime1 == mtime2
 
+compareBy :: InodeComparisonType -> InodeCache -> InodeCache -> Bool
+compareBy Strongly = compareStrong
+compareBy Weakly = compareWeak
+
+{- For use in a Map; it's determined at creation time whether this
+ - uses strong or weak comparison for Eq. -}
+data InodeCacheKey = InodeCacheKey InodeComparisonType InodeCachePrim
+	deriving (Ord)
+
+instance Eq InodeCacheKey where
+	(InodeCacheKey ctx x) == (InodeCacheKey cty y) =
+		compareBy (maximum [ctx,cty]) (InodeCache x ) (InodeCache y)
+
+inodeCacheToKey :: InodeComparisonType -> InodeCache -> InodeCacheKey 
+inodeCacheToKey ct (InodeCache prim) = InodeCacheKey ct prim
+
 showInodeCache :: InodeCache -> String
-showInodeCache (InodeCache inode size mtime) = unwords
+showInodeCache (InodeCache (InodeCachePrim inode size mtime)) = unwords
 	[ show inode
 	, show size
 	, show mtime
@@ -29,23 +58,34 @@
 
 readInodeCache :: String -> Maybe InodeCache
 readInodeCache s = case words s of
-	(inode:size:mtime:_) -> InodeCache
-		<$> readish inode
-		<*> readish size
-		<*> readish mtime
+	(inode:size:mtime:_) ->
+		let prim = InodeCachePrim
+			<$> readish inode
+			<*> readish size
+			<*> readish mtime
+		in InodeCache <$> prim
 	_ -> Nothing
 
--- for quickcheck
-prop_read_show_inodecache :: InodeCache -> Bool
-prop_read_show_inodecache c = readInodeCache (showInodeCache c) == Just c
-
 genInodeCache :: FilePath -> IO (Maybe InodeCache)
 genInodeCache f = catchDefaultIO Nothing $ toInodeCache <$> getFileStatus f
 
 toInodeCache :: FileStatus -> Maybe InodeCache
 toInodeCache s
-	| isRegularFile s = Just $ InodeCache
+	| isRegularFile s = Just $ InodeCache $ InodeCachePrim
 		(fileID s)
 		(fileSize s)
 		(modificationTime s)
 	| otherwise = Nothing
+
+instance Arbitrary InodeCache where
+	arbitrary =
+		let prim = InodeCachePrim
+			<$> arbitrary
+			<*> arbitrary
+			<*> arbitrary
+		in InodeCache <$> prim
+
+prop_read_show_inodecache :: InodeCache -> Bool
+prop_read_show_inodecache c = case readInodeCache (showInodeCache c) of
+	Nothing -> False
+	Just c' -> compareStrong c c'
diff --git a/Utility/Kqueue.hs b/Utility/Kqueue.hs
--- a/Utility/Kqueue.hs
+++ b/Utility/Kqueue.hs
@@ -18,7 +18,7 @@
 ) where
 
 import Common
-import Utility.Types.DirWatcher
+import Utility.DirWatcher.Types
 
 import System.Posix.Types
 import Foreign.C.Types
diff --git a/Utility/LogFile.hs b/Utility/LogFile.hs
--- a/Utility/LogFile.hs
+++ b/Utility/LogFile.hs
@@ -13,22 +13,24 @@
 
 openLog :: FilePath -> IO Fd
 openLog logfile = do
-	rotateLog logfile 0
+	rotateLog logfile
 	openFd logfile WriteOnly (Just stdFileMode)
 		defaultFileFlags { append = True }
 
-rotateLog :: FilePath -> Int -> IO ()
-rotateLog logfile num
-	| num > maxLogs = return ()
-	| otherwise = whenM (doesFileExist currfile) $ do
-		rotateLog logfile (num + 1)
-		renameFile currfile nextfile
+rotateLog :: FilePath -> IO ()
+rotateLog logfile = go 0
   where
-	currfile = filename num
-	nextfile = filename (num + 1)
-	filename n
-		| n == 0 = logfile
-		| otherwise = rotatedLog logfile n
+	go num
+		| num > maxLogs = return ()
+		| otherwise = whenM (doesFileExist currfile) $ do
+			go (num + 1)
+			renameFile currfile nextfile
+	  where
+		currfile = filename num
+		nextfile = filename (num + 1)
+		filename n
+			| n == 0 = logfile
+			| otherwise = rotatedLog logfile n
 
 rotatedLog :: FilePath -> Int -> FilePath
 rotatedLog logfile n = logfile ++ "." ++ show n
@@ -40,3 +42,13 @@
 
 maxLogs :: Int
 maxLogs = 9
+
+redirLog :: Fd -> IO ()
+redirLog logfd = do
+	mapM_ (redir logfd) [stdOutput, stdError]
+	closeFd logfd
+
+redir :: Fd -> Fd -> IO ()
+redir newh h = do
+	closeFd h
+	void $ dupTo newh h
diff --git a/Utility/QuickCheck.hs b/Utility/QuickCheck.hs
--- a/Utility/QuickCheck.hs
+++ b/Utility/QuickCheck.hs
@@ -1,4 +1,4 @@
-{- QuickCheck instances
+{- QuickCheck with additional instances
  -
  - Copyright 2012 Joey Hess <joey@kitenet.net>
  -
@@ -8,11 +8,19 @@
 {-# OPTIONS_GHC -fno-warn-orphans #-}
 {-# LANGUAGE TypeSynonymInstances #-}
 
-module Utility.QuickCheck where
+module Utility.QuickCheck
+	( module X
+	, module Utility.QuickCheck
+	) where
 
-import Test.QuickCheck
+import Test.QuickCheck as X
 import Data.Time.Clock.POSIX
 import System.Posix.Types
+import qualified Data.Map as M
+import Control.Applicative
+
+instance (Arbitrary k, Arbitrary v, Eq k, Ord k) => Arbitrary (M.Map k v) where
+	arbitrary = M.fromList <$> arbitrary
 
 {- Times before the epoch are excluded. -}
 instance Arbitrary POSIXTime where
diff --git a/Utility/TSet.hs b/Utility/TSet.hs
--- a/Utility/TSet.hs
+++ b/Utility/TSet.hs
@@ -1,6 +1,6 @@
 {- Transactional sets
  -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2012, 2013 Joey Hess <joey@kitenet.net>
  -}
 
 module Utility.TSet where
@@ -11,18 +11,20 @@
 
 type TSet = TChan
 
-runTSet :: STM a -> IO a
-runTSet = atomically
-
-newTSet :: IO (TSet a)
-newTSet = atomically newTChan
+newTSet :: STM (TSet a)
+newTSet = newTChan
 
 {- Gets the contents of the TSet. Blocks until at least one item is
  - present. -}
-getTSet :: TSet a -> IO [a]
-getTSet tset = runTSet $ do
+getTSet :: TSet a -> STM [a]
+getTSet tset = do
 	c <- readTChan tset
-	go [c]
+	l <- readTSet tset
+	return $ c:l
+
+{- Gets anything currently in the TSet, without blocking. -}
+readTSet :: TSet a -> STM [a]
+readTSet tset = go []
   where
 	go l = do
 		v <- tryReadTChan tset
@@ -31,9 +33,9 @@
 			Just c -> go (c:l)
 
 {- Puts items into a TSet. -}
-putTSet :: TSet a -> [a] -> IO ()
-putTSet tset vs = runTSet $ mapM_ (writeTChan tset) vs
+putTSet :: TSet a -> [a] -> STM ()
+putTSet tset vs = mapM_ (writeTChan tset) vs
 
 {- Put a single item into a TSet. -}
-putTSet1 :: TSet a -> a -> IO ()
-putTSet1 tset v = void $ runTSet $ writeTChan tset v
+putTSet1 :: TSet a -> a -> STM ()
+putTSet1 tset v = void $ writeTChan tset v
diff --git a/Utility/ThreadScheduler.hs b/Utility/ThreadScheduler.hs
--- a/Utility/ThreadScheduler.hs
+++ b/Utility/ThreadScheduler.hs
@@ -1,6 +1,6 @@
 {- thread scheduling
  -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
+ - Copyright 2012, 2013 Joey Hess <joey@kitenet.net>
  - Copyright 2011 Bas van Dijk & Roel van Dijk
  -
  - Licensed under the GNU GPL version 3 or higher.
@@ -14,6 +14,7 @@
 
 import Control.Concurrent
 import System.Posix.Signals
+import Data.Time.Clock
 #ifndef __ANDROID__
 import System.Posix.Terminal
 #endif
@@ -21,6 +22,8 @@
 newtype Seconds = Seconds { fromSeconds :: Int }
 	deriving (Eq, Ord, Show)
 
+type Microseconds = Integer
+
 {- Runs an action repeatedly forever, sleeping at least the specified number
  - of seconds in between. -}
 runEvery :: Seconds -> IO a -> IO a
@@ -30,8 +33,6 @@
 
 threadDelaySeconds :: Seconds -> IO ()
 threadDelaySeconds (Seconds n) = unboundDelay (fromIntegral n * oneSecond)
-  where
-	oneSecond = 1000000 -- microseconds
 
 {- Like threadDelay, but not bounded by an Int.
  -
@@ -42,7 +43,7 @@
  - Taken from the unbounded-delay package to avoid a dependency for 4 lines
  - of code.
  -}
-unboundDelay :: Integer -> IO ()
+unboundDelay :: Microseconds -> IO ()
 unboundDelay time = do
 	let maxWait = min time $ toInteger (maxBound :: Int)
 	threadDelay $ fromInteger maxWait
@@ -61,3 +62,6 @@
   where
 	check sig lock = void $
 		installHandler sig (CatchOnce $ putMVar lock ()) Nothing
+
+oneSecond :: Microseconds
+oneSecond = 1000000
diff --git a/Utility/Types/DirWatcher.hs b/Utility/Types/DirWatcher.hs
deleted file mode 100644
--- a/Utility/Types/DirWatcher.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-{- generic directory watching types
- -
- - Copyright 2012 Joey Hess <joey@kitenet.net>
- -
- - Licensed under the GNU GPL version 3 or higher.
- -}
-
-{-# LANGUAGE CPP #-}
-
-module Utility.Types.DirWatcher where
-
-import Common
-
-type Hook a = Maybe (a -> Maybe FileStatus -> IO ())
-
-data WatchHooks = WatchHooks
-	{ addHook :: Hook FilePath
-	, addSymlinkHook :: Hook FilePath
-	, delHook :: Hook FilePath
-	, delDirHook :: Hook FilePath
-	, errHook :: Hook String -- error message
-	, modifyHook :: Hook FilePath
-	}
-
-mkWatchHooks :: WatchHooks
-mkWatchHooks = WatchHooks Nothing Nothing Nothing Nothing Nothing Nothing
diff --git a/Utility/Url.hs b/Utility/Url.hs
--- a/Utility/Url.hs
+++ b/Utility/Url.hs
@@ -34,10 +34,10 @@
 {- Checks that an url exists and could be successfully downloaded,
  - also returning its size if available. -}
 exists :: URLString -> Headers -> IO (Bool, Maybe Integer)
-exists url headers = case parseURI url of
+exists url headers = case parseURIRelaxed url of
 	Just u
 		| uriScheme u == "file:" -> do
-			s <- catchMaybeIO $ getFileStatus (uriPath u)
+			s <- catchMaybeIO $ getFileStatus (unEscapeString $ uriPath u)
 			case s of
 				Just stat -> return (True, Just $ fromIntegral $ fileSize stat)
 				Nothing -> dne
@@ -70,13 +70,18 @@
  - so is preferred.) Which program to use is determined at run time; it
  - would not be appropriate to test at configure time and build support
  - for only one in.
- -
- - Curl is always used for file:// urls, as wget does not support them.
  -}
 download :: URLString -> Headers -> [CommandParam] -> FilePath -> IO Bool
-download url headers options file
-	| "file://" `isPrefixOf` url = curl
-	| otherwise = ifM (inPath "wget") (wget , curl)
+download url headers options file = 
+	case parseURIRelaxed url of
+		Just u
+			| uriScheme u == "file:" -> do
+				-- curl does not create destination file
+				-- for an empty file:// url, so pre-create
+				writeFile file ""
+				curl
+			| otherwise -> ifM (inPath "wget") (wget , curl)
+		_ -> return False
   where
 	headerparams = map (\h -> Param $ "--header=" ++ h) headers
 	wget = go "wget" $ headerparams ++ [Params "-c -O"]
@@ -93,3 +98,7 @@
 get :: URLString -> Headers -> IO String
 get url headers = readProcess "curl" $
 	["-s", "-L", url] ++ concatMap (\h -> ["-H", h]) headers
+
+{- Allows for spaces and other stuff in urls, properly escaping them. -}
+parseURIRelaxed :: URLString -> Maybe URI
+parseURIRelaxed = parseURI . escapeURIString isAllowedInURI
diff --git a/Utility/WebApp.hs b/Utility/WebApp.hs
--- a/Utility/WebApp.hs
+++ b/Utility/WebApp.hs
@@ -56,9 +56,15 @@
 runWebApp :: Wai.Application -> (SockAddr -> IO ()) -> IO ()
 runWebApp app observer = do
 	sock <- localSocket
-	void $ forkIO $ runSettingsSocket defaultSettings sock app
+	void $ forkIO $ runSettingsSocket webAppSettings sock app
 	observer =<< getSocketName sock
 
+webAppSettings :: Settings
+webAppSettings = defaultSettings
+	-- disable buggy sloworis attack prevention code
+	{ settingsTimeout = 30 * 60
+	}
+
 {- Binds to a local socket, selecting any free port.
  -
  - Prefers to bind to the ipv4 address rather than the ipv6 address
@@ -134,8 +140,17 @@
 		Left e -> error $ "failed to generate random key: " ++ show e
 		Right (s, _) -> case CS.initKey s of
 			Left e -> error $ "failed to initialize key: " ++ show e
-			Right key -> return $ Just $
-				Yesod.clientSessionBackend key 120
+			Right key -> use key
+  where
+	timeout = 120 * 60 -- 120 minutes
+	use key =
+#if MIN_VERSION_yesod(1,1,7)
+		Just . Yesod.clientSessionBackend2 key . fst
+			<$> Yesod.clientSessionDateCacher timeout
+#else
+		return $ Just $
+			Yesod.clientSessionBackend key timeout
+#endif
 
 {- Generates a random sha512 string, suitable to be used for an
  - authentication secret. -}
diff --git a/Utility/Yesod.hs b/Utility/Yesod.hs
--- a/Utility/Yesod.hs
+++ b/Utility/Yesod.hs
@@ -7,23 +7,17 @@
 
 {-# LANGUAGE CPP #-}
 
-#if defined VERSION_yesod_default
-#if ! MIN_VERSION_yesod_default(1,1,0)
-#define WITH_OLD_YESOD
-#endif
-#endif
-
 module Utility.Yesod where
 
 import Yesod.Default.Util
 import Language.Haskell.TH.Syntax
-#ifndef WITH_OLD_YESOD
+#if MIN_VERSION_yesod_default(1,1,0)
 import Data.Default (def)
 import Text.Hamlet
 #endif
 
 widgetFile :: String -> Q Exp
-#ifdef WITH_OLD_YESOD
+#if ! MIN_VERSION_yesod_default(1,1,0)
 widgetFile = widgetFileNoReload
 #else
 widgetFile = widgetFileNoReload $ def
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,78 @@
+git-annex (4.20130314) unstable; urgency=low
+
+  * Bugfix: git annex add, when ran without any file or directory specified,
+    should add files in the current directory, but not act on unlocked files
+    elsewhere in the tree.
+  * Bugfix: drop --from an unavailable remote no longer updates the location
+    log, incorrectly, to say the remote does not have the key.
+  * Bugfix: If the UUID of a remote is not known, prevent --from, --to,
+    and other ways of specifying remotes by name from selecting it,
+    since it is not possible to sanely use it.
+  * Bugfix: Fix bug in inode cache sentinal check, which broke
+    copying to local repos if the repo being copied from had moved
+    to a different filesystem or otherwise changed all its inodes
+
+  * Switch from using regex-compat to regex-tdfa, as the C regex library
+    is rather buggy.
+  * status: Can now be run with a directory path to show only the
+    status of that directory, rather than the whole annex.
+  * Added remote.<name>.annex-gnupg-options setting.
+    Thanks, guilhem for the patch.
+  * addurl: Add --relaxed option.
+  * addurl: Escape invalid characters in urls, rather than failing to
+    use an invalid url.
+  * addurl: Properly handle url-escaped characters in file:// urls.
+
+  * assistant: Fix dropping content when a file is moved to an archive
+    directory, and getting contennt when a file is moved back out.
+  * assistant: Fix bug in direct mode that could occur when a symlink is
+    moved out of an archive directory, and resulted in the file not being
+    set to direct mode when it was transferred.
+  * assistant: Generate better commits for renames.
+  * assistant: Logs are rotated to avoid them using too much disk space.
+  * assistant: Avoid noise in logs from git commit about typechanged
+    files in direct mode repositories.
+  * assistant: Set gc.auto=0 when creating repositories to prevent
+    automatic commits from causing git-gc runs.
+  * assistant: If gc.auto=0, run git-gc once a day, packing loose objects
+    very non-aggressively.
+  * assistant: XMPP git pull and push requests are cached and sent when
+    presence of a new client is detected.
+  * assistant: Sync with all git remotes on startup.
+  * assistant: Get back in sync with XMPP remotes after network reconnection,
+    and on startup.
+  * assistant: Fix syncing after XMPP pairing.
+  * assistant: Optimised handling of renamed files in direct mode,
+    avoiding re-checksumming.
+  * assistant: Detects most renames, including directory renames, and
+    combines all their changes into a single commit.
+  * assistant: Fix ~/.ssh/git-annex-shell wrapper to work when the
+    ssh key does not force a command.
+  * assistant: Be smarter about avoiding unncessary transfers.
+
+  * webapp: Work around bug in Warp's slowloris attack prevention code,
+    that caused regular browsers to stall when they reuse a connection
+    after leaving it idle for 30 seconds.
+    (See https://github.com/yesodweb/wai/issues/146)
+  * webapp: New preferences page allows enabling/disabling debug logging
+    at runtime, as well as configuring numcopies and diskreserve.
+  * webapp: Repository costs can be configured by dragging repositories around
+    in the repository list.
+  * webapp: Proceed automatically on from "Configure jabber account"
+    to pairing.
+  * webapp: Only show up to 10 queued transfers.
+  * webapp: DTRT when told to create a git repo that already exists.
+  * webapp: Set locally paired repositories to a lower cost than other
+    network remotes.
+
+  * Run ssh with -T to avoid tty allocation and any login scripts that
+    may do undesired things with it.
+  * Several improvements to Makefile and cabal file. Thanks, Peter Simmons
+  * Stop depending on testpack.
+  * Android: Enable test suite. 
+
+ -- Joey Hess <joeyh@debian.org>  Thu, 14 Mar 2013 15:29:20 -0400
+
 git-annex (4.20130227) unstable; urgency=low
 
   * annex.version is now set to 4 for direct mode repositories.
diff --git a/debian/control b/debian/control
--- a/debian/control
+++ b/debian/control
@@ -10,11 +10,11 @@
 	libghc-hslogger-dev,
 	libghc-pcre-light-dev,
 	libghc-sha-dev,
+	libghc-regex-tdfa-dev,
 	libghc-dataenc-dev,
 	libghc-utf8-string-dev,
 	libghc-hs3-dev (>= 0.5.6),
 	libghc-dav-dev (>= 0.3) [amd64 i386 kfreebsd-amd64 kfreebsd-i386 sparc],
-	libghc-testpack-dev,
 	libghc-quickcheck2-dev,
 	libghc-monad-control-dev (>= 0.3),
 	libghc-lifted-base-dev,
diff --git a/debian/copyright b/debian/copyright
--- a/debian/copyright
+++ b/debian/copyright
@@ -6,8 +6,17 @@
 License: GPL-3+
 
 Files: Assistant/WebApp.hs Assistant/WebApp/* templates/* static/*
-Copyright: © 2012 Joey Hess <joey@kitenet.net>
+Copyright: © 2012-2013 Joey Hess <joey@kitenet.net>
 License: AGPL-3+
+
+Files: Utility/ThreadScheduler.hs
+Copyright: 2011 Bas van Dijk & Roel van Dijk
+           2012 Joey Hess <joey@kitenet.net>
+License: GPL-3+
+
+Files: Utility/Gpg/Types.hs
+Copyright: 2013 guilhem <guilhem@fripost.org>
+License: GPL-3+
 
 Files: doc/logo* */favicon.ico standalone/osx/git-annex.app/Contents/Resources/git-annex.icns
 Copyright: 2007 Henrik Nyh <http://henrik.nyh.se/>
diff --git a/doc/assistant.mdwn b/doc/assistant.mdwn
--- a/doc/assistant.mdwn
+++ b/doc/assistant.mdwn
@@ -11,26 +11,13 @@
 Note that the git-annex assistant is still beta quality code. See
 the [[release_notes]] for known infelicities and upgrade instructions.
 
-## quick start
-
-To get started with the git-annex assistant, just pick it from
-your system's list of applications.
-
-[[!img assistant/menu.png]]
-[[!img assistant/osx-app.png]]
-
-It'll prompt you to set up a folder:
-
-[[!img assistant/makerepo.png]]
-
-Then any changes you make to its folder will automatically be committed to
-git, and synced to repositories on other computers. You can use the
-interface to add repositories and control the git-annex assistant.
+## intro screencast
 
-[[!img assistant/running.png]]
+[[!inline feeds=no template=bare pages=videos/git-annex_assistant_introduction]]
 
 ## documentation
 
+* [[Basic usage|quickstart]]
 * Want to make two nearby computers share the same synchronised folder?
   Follow the [[pairing_walkthrough]].
 * Want to share a synchronised folder with a friend?
diff --git a/doc/assistant/preferences.png b/doc/assistant/preferences.png
new file mode 100644
Binary files /dev/null and b/doc/assistant/preferences.png differ
diff --git a/doc/assistant/quickstart.mdwn b/doc/assistant/quickstart.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/assistant/quickstart.mdwn
@@ -0,0 +1,15 @@
+To get started with the git-annex assistant, just pick it from
+your system's list of applications.
+
+[[!img assistant/menu.png]]
+[[!img assistant/osx-app.png]]
+
+It'll prompt you to set up a folder:
+
+[[!img assistant/makerepo.png]]
+
+Then any changes you make to its folder will automatically be committed to
+git, and synced to repositories on other computers. You can use the
+interface to add repositories and control the git-annex assistant.
+
+[[!img assistant/running.png]]
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,14 @@
+## version 4.20130314
+
+This version makes a great many improvements and bugfixes, and is
+a recommended upgrade.
+
+If you have already used the webapp to locally pair two computers,
+a bug caused the paired repository to not be given an appropriate cost.
+To fix this, go into the Repositories page in the webapp, and drag the
+repository for the locally paired computer to come before any repositories
+that it's more expensive to transfer data to.
+
 ## version 4.20130227
 
 This release fixes a bug with globbing that broke preferred content expressions.
diff --git a/doc/bugs/Assistant_dropping_from_backup_repo.mdwn b/doc/bugs/Assistant_dropping_from_backup_repo.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Assistant_dropping_from_backup_repo.mdwn
@@ -0,0 +1,23 @@
+Setup:
+
+* fresh install of Debian Wheezy with git-annex 4.20130227 pulled in from unstable
+
+Steps:
+
+* clone existing repository and activate assistant
+* Have USB drive, U, with repository group `backup` and preferred content string `standard`
+
+Expected:
+
+* Assistant never ever tries to drop anything from U
+
+Actual:
+
+* Assistant immediately tries to drop files from U; fortunately I didn't have the USB drive plugged in
+* Changing the preferred content string of U to `present or include=*` stops the dropping, but this was never required before
+
+Additional information:
+
+* The files that the Assistant started trying to drop were, I believe, the first (alphabetically) files in my repository to contain non-ascii characters in their file names (some French accented letters)
+
+Thanks.
diff --git a/doc/bugs/Complete_failure_trying_to_unannex_a_large_annex.mdwn b/doc/bugs/Complete_failure_trying_to_unannex_a_large_annex.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Complete_failure_trying_to_unannex_a_large_annex.mdwn
@@ -0,0 +1,53 @@
+I really don't know what's happened here, I just did `git annex unannex .` in a very large annex:
+
+    unannex Inbox/Lolcat.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Lolcat.jpg (Recording state in git...)
+    ok
+    unannex Inbox/May 2012 Photo Stream/120502_0004.JPG (Recording state in git...)
+    ok
+    unannex Inbox/May 2012 Photo Stream/120518_0005.JPG (Recording state in git...)
+    ok
+    unannex Inbox/May 2012 Photo Stream/120523_0006.JPG (Recording state in git...)
+    ok
+    unannex Inbox/May 2012 Photo Stream/120523_0007.JPG (Recording state in git...)
+    ok
+    unannex Inbox/My boyfriend of 7 years and I are both physicists. Here's how he proposed to me. - Imgur.jpg (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121102_0035.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121102_0036.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121102_0037.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121102_0038.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121102_0039.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121103_0040.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121104_0041.JPG (Recording state in git...)
+    ok
+    unannex Inbox/Nov 2012 Photo Stream/121105_0042.JPG (Recording state in git...)
+    error: bad index file sha1 signature
+    fatal: index file corrupt
+    git-annex: failed to read sha from git write-tree
+    git-annex: git commit [Param "-q",Params "-m",Param "content removed from git annex",Param "--",File "Inbox/Nov 2012 Photo Stream/121105_0042.JPG"] failed
+    Vulcan:~/Pictures $ ga unannex .
+    unannex Inbox/Nov 2012 Photo Stream/121109_0043.JPG error: bad index file sha1 signature
+    fatal: index file corrupt
+
+    git-annex: fd:12: hClose: resource vanished (Broken pipe)
+    failed
+    git-annex: pre-commit: 1 failed
+    git-annex: git commit [Param "-q",Params "-m",Param "content removed from git annex",Param "--",File "Inbox/Nov 2012 Photo Stream/121109_0043.JPG"] failed
+    Vulcan:~/Pictures $ ga -F unannex .
+    unannex Inbox/Nov 2012 Photo Stream/121124_0044.JPG error: bad index file sha1 signature
+    fatal: index file corrupt
+
+    git-annex: fd:12: hClose: resource vanished (Broken pipe)
+    failed
+    git-annex: pre-commit: 1 failed
+    git-annex: git commit [Param "-q",Params "-m",Param "content removed from git annex",Param "--",File "Inbox/Nov 2012 Photo Stream/121124_0044.JPG"] failed
+
+I guess now I'll just try to unlink the symlinks by hand, and drop the `.git` directory?
diff --git a/doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files.mdwn b/doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Direct_mode_keeps_re-checksuming_duplicated_files.mdwn
@@ -0,0 +1,22 @@
+##What steps will reproduce the problem?
+
+    mkdir test
+    git init
+    git annex init "test"
+    echo "test" > a
+    echo "test" > b
+    git annex add a b
+    git annex sync
+    git annex direct
+    git annex sync | grep add
+    git annex sync | grep add
+
+##What is the expected output? What do you see instead?
+
+The last two syncs shouldn't need to add or checksum anything.
+Firstly, the output is very confusing because the files have already been added.
+Secondly, the sync can take quite a while if you have lots of duplicates or a lot of files that are incidentally similar.
+
+##What version of git-annex are you using? On what operating system?
+
+git-annex version: 4.20130227 on Archlinux
diff --git a/doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds.mdwn b/doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Out_of_memory_error_in_fsck_whereis_find_and_status_cmds.mdwn
@@ -0,0 +1,78 @@
+Before I start on what's gone wrong, many thanks for a great program: finally a way of finding out where I've put all those files, and I enjoyed your talk in Australia. Not quite to New Zealand, but a good start :-)
+
+To play with git-annex, I decided to convert my ~/Downloads directory to a git-annex repository, as there were a wide variety of files in it, mostly easily replaceable, and also handy to have on multiple machines. In hindsight, probably wasn't a great idea, as I'd regularly forget it was a git-annex repo and move files and directories out manually, which caused all sorts of fun trying to sort out the dead symlinks. Then I after one update, the repository format changed, from WORM to xxx256 format which meant there were now both sorts in the GA object store.
+
+More recently I'd tried converting the repo to direct mode, you get the idea: lots of playing with git-annex commands, and now that I think about it, possibly some git commands too trying to repair missing files.
+
+Anyway I've ended up with a 27GB git-annex repo that now manages to kill git-annex whenever I try to check it using "git-annex fsck". 
+
+Not only does the fsck subcommand cause it to die, but also "find", "whereis" and "status". It dies on the same file (for find/whereis/fsck).
+
+e.g.
+
+    ... lots of stuff deleted ...
+    whereis 1wolf14.zip (2 copies) 
+            051f0b00-e265-11e1-894e-3b0b3f3844f2 -- Laptop
+            2c4e11e0-a1b4-11e1-9a02-73e17b04c00f -- here (myPC - Downloads)
+    ok
+    git-annex: out of memory (requested 2097152 bytes)
+
+Now "git status" for some repo data:
+
+    myPC:~/Downloads$ git annex status
+    supported backends: SHA256E SHA1E SHA512E SHA224E SHA384E SHA256 SHA1 SHA512 SHA224 SHA384 WORM URL
+    supported remote types: git S3 bup directory rsync web webdav glacier hook
+    repository mode: direct
+    trusted repositories: 0
+    semitrusted repositories: 4
+            00000000-0000-0000-0000-000000000001 -- web
+            051f0b00-e265-11e1-894e-3b0b3f3844f2 -- Laptop
+            2c4e11e0-a1b4-11e1-9a02-73e17b04c00f -- here (myPC - Downloads)
+            48fbe52a-a1b3-11e1-bb80-ebc15118871d -- netbk
+    untrusted repositories: 0
+    dead repositories: 0
+    transfers in progress: none
+    available local disk space: 100 gigabytes (+1 megabyte reserved)
+    local annex keys: 1719
+    local annex size: 27 gigabytes
+    known annex keys: git-annex: out of memory (requested 2097152 bytes)
+
+It always seems to die at about 3.5GB memory usage. This is running on Ubuntu 12.04, using the latest GA release built using cabal:
+
+    git-annex version: 4.20130227
+    local repository version: 3
+    default repository version: 3
+    supported repository versions: 3 4
+    upgrade supported from repository versions: 0 1 2
+
+There are also dead symlinks that point to directories that have meta-data but not the symlink target (manually line-wrapped):
+
+    myPC:~/Downloads$ ls -l precise*
+    lrwxrwxrwx 1 nino nino 194 Oct 21 12:48 precise-dvd-i386.iso -> 
+                 .git/annex/objects/0x/Xz/SHA256-s3590631424--
+                 b08ecdd4846948ec076b23afae7f87be9cfba5218fb9ba4160f26c0b8d4b5dd0/
+                 SHA256-s3590631424--b08ecdd4846948ec076b23afae7f87be9cfba5218fb9ba4160f26c0b8d4b5dd0
+
+But looking in the symlink destination directory, there's no corresponding object, only metadata:
+
+    myPC:~/Downloads/.git/annex/objects/0x/Xz/SHA256-s3590631424--b08ecdd4846948ec076b23afae7f87be9cfba5218fb9ba4160f26c0b8d4b5dd0$ ls -l
+    total 8
+    -rw-rw-r-- 1 nino nino 30 Jan  8 23:15 SHA256-s3590631424--b08ecdd4846948ec076b23afae7f87be9cfba5218fb9ba4160f26c0b8d4b5dd0.cache
+    -rw-rw-r-- 1 nino nino 49 Jan  8 23:15 SHA256-s3590631424--b08ecdd4846948ec076b23afae7f87be9cfba5218fb9ba4160f26c0b8d4b5dd0.map
+
+But there is another version somewhere else.
+
+    -r--r--r-- 1 nino nino 3590631424 Mar 18  2012 ./.git/annex/objects/Kj/wM/WORM-s3590631424-m1331991509
+                        --precise-dvd-i386.iso/WORM-s3590631424-m1331991509--precise-dvd-i386.iso
+
+This actual file does exist in the "Used" directory:
+
+    -rw-r--r-- 1 nino nino 3.4G May 27  2012 precise-dvd-i386.iso
+
+I'm not so worried about the mangled repo - it's quite possibly because of clueless git/git-annex command usage - but the inability to use the fsck command is concerning
+
+I could just uninit everything, but as it dies prematurely, I'm not certain that all the contents would be restored.
+Any thoughts on how I can get git-annex (esp. fsck) to complete would be appreciated.
+
+Thanks
+Giovanni
diff --git a/doc/bugs/Partial_direct__47__indirect_repo.mdwn b/doc/bugs/Partial_direct__47__indirect_repo.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Partial_direct__47__indirect_repo.mdwn
@@ -0,0 +1,24 @@
+Setup:
+
+* Fresh install of Debian Wheezy on machines A & B, git-annex 4.20130227 pulled in from unstable
+* On both machines, clone old repository which contains both annexed files and a three small files checked straight into git
+
+Steps:
+
+* On both machines, use webapp to create `~/.config/git-annex/autostart` by just firing it up and typing in location of existing repository
+* Move a new file into B's annex, in a subdirectory that is preferred on both A & B
+
+Expected:
+
+* The new file is copied over to A and everything remains in indirect mode
+* Three files checked straight into git remain checked straight into git (see below for why this is a variant on [[bugs/Switching_between_direct_and_indirect_stomps_on___39__regular__39___git_files/]])
+
+Actual:
+
+* New file copied over but seems to be in direct mode, while all the other content that is present is still symlinked
+* Files checked into git converted to direct mode files too (can tell this has happened by following step:)
+* Typing `git annex indirect` on A & B shows conversion of precisely four files (three files originally checked into git and new file added to B ) back to indirect
+
+Thanks.
+
+> [[done]], webapp now avoids changing existing repos here. --[[Joey]]
diff --git a/doc/bugs/Provide_64-bit_standalone_build.mdwn b/doc/bugs/Provide_64-bit_standalone_build.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Provide_64-bit_standalone_build.mdwn
@@ -0,0 +1,1 @@
+The 32-bit standalone build appears to require two libraries (lib32-libyaml and lib32-gsasl) that are not available on Arch Linux. [See the comments on the AUR package](https://aur.archlinux.org/packages/git-annex-bin/). I'd appreciate it if you could bring back the 64-bit build.
diff --git a/doc/bugs/Renamed_special_remote_cannot_be_reactivated_by_the_webapp.mdwn b/doc/bugs/Renamed_special_remote_cannot_be_reactivated_by_the_webapp.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Renamed_special_remote_cannot_be_reactivated_by_the_webapp.mdwn
@@ -0,0 +1,28 @@
+Setup:
+
+* fresh install of Debian Wheezy with git-annex 4.20130227 pulled in from unstable
+* clone existing repository and activate assistant
+* repository has encrypted rsync remote originally setup with the name `metaarray`
+* this remote was renamed to `ma` a long time ago, using the webapp
+* had to perform this rename on each client
+
+Steps:
+
+* attempt to reactivate special remote using webapp repositories page, on reinstalled machine
+
+Expected:
+
+* special remote starts working
+* renaming special remotes ought to survive clones
+
+Actual:
+
+* firstly, special remote activation page has blank hostname box and the hostname of the machine is in the username box
+* form gives error "cannot change encryption type of existing remote"
+
+Workaround:
+
+* execute `git annex initremote metaarray`
+* rename `metaarray` to `ma` again using the webapp
+
+Perhaps the renaming of the remote not surviving clones is unavoidable, but the webapp should be able to cope with the situation.  Thanks.
diff --git a/doc/bugs/Rsync_encrypted_remote_asks_for_ssh_key_password_for_each_file.mdwn b/doc/bugs/Rsync_encrypted_remote_asks_for_ssh_key_password_for_each_file.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/Rsync_encrypted_remote_asks_for_ssh_key_password_for_each_file.mdwn
@@ -0,0 +1,26 @@
+What steps will reproduce the problem?
+
+Add an encrypted rsync remote by it's 'Host' value in ~/.ssh/config.
+
+eg.:
+
+cat ~/.ssh/config | grep Host
+
+    Host serverNick
+
+git annex initremote rsyncRemote type=rsync rsyncurl=serverNick:/home/USER/Music encryption=USER@gmail.com
+
+git annex copy some\ artist --to serverNick
+
+
+What is the expected output? What do you see instead?
+
+I'd expect it to remember the key password like a normal ssh remote.  Instead I get asked for the key password 3 times for each file in the folder.
+
+What version of git-annex are you using? On what operating system?
+
+3.20130216.  Arch x64 (up to date as of 2013-03-07)
+
+Please provide any additional information below.
+
+
diff --git a/doc/bugs/addurl_--relaxed_with_--file_doesn__39__t_actually_relax.mdwn b/doc/bugs/addurl_--relaxed_with_--file_doesn__39__t_actually_relax.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/addurl_--relaxed_with_--file_doesn__39__t_actually_relax.mdwn
@@ -0,0 +1,26 @@
+It appears like addurl --relaxed if used incombination with --file doesn't actually relax.
+
+(I'm interested in quickly adding links to an extremely large set of files (and for a large set of revisions), and the fact that addurl takes a second or so per file makes this impossible performance-wise.)
+
+What steps will reproduce the problem? (Well, this isn't the problem per see, but it illustrates that it does checking)
+
+    $ echo foo > foo
+    $ git annex add foo
+    $ git annex addurl --relaxed http://lambda.haskell.org/platform/download/2012.4.0.0/haskell-platform-2012.4.0.0.tar.gz --file foo
+    addurl foo 
+      failed to verify url: http://lambda.haskell.org/platform/download/2012.4.0.0/haskell-platform-2012.4.0.0.tar.gz
+    failed
+    git-annex: addurl: 1 failed
+
+What version of git-annex are you using? On what operating system?
+
+Debian Sid
+
+    git-annex version: 4.20130228
+    local repository version: 3
+    default repository version: 3
+    supported repository versions: 3 4
+    upgrade supported from repository versions: 0 1 2
+    build flags: Assistant Webapp Pairing Testsuite S3 WebDAV Inotify DBus XMPP DNS
+
+> Missed the case of adding an url to an existing file. [[done]] --[[Joey]] 
diff --git a/doc/bugs/annex_get_fails:___34__No_such_file_or_directory__34__.mdwn b/doc/bugs/annex_get_fails:___34__No_such_file_or_directory__34__.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/annex_get_fails:___34__No_such_file_or_directory__34__.mdwn
@@ -0,0 +1,68 @@
+**What steps will reproduce the problem?**
+
+I did a basic git annex setup with two repositories talking to each other.  They are on the same macine, but I identified them via the hostname, because I intend to set up my production systems on two machines.  Since I am new to annex, I'll reproduce the full sequence of commands to create the repos and sync them.  *I* noticed the trouble at the last step, when `git annex get` failed.
+
+Here is the full sequence of commands:
+
+    >>> cd /scr/wandschn/hackNtest/distributed/nyc/STU_files
+    >>> git init
+    >>> git annex init nyc
+    >>> cd /scr/wandschn/hackNtest/distributed/pdx
+
+    >>> git clone xerxes:/scr/wandschn/hackNtest/distributed/nyc/STU_files
+    >>> git annex init pdx
+    >>> git remote add nyc xerxes:/scr/wandschn/hackNtest/distributed/nyc/STU_files
+
+    >>> cd /scr/wandschn/hackNtest/distributed/nyc/STU_files
+    >>> git remote add pdx xerxes:/scr/wandschn/hackNtest/distributed/pdx/STU_files
+
+    >>> mkdir shared
+    >>> cp ../../../files/shared/* shared/.
+    >>> git annex add shared
+    >>> git commit -a -m "initial add of shared files"
+
+    >>> cd /scr/wandschn/hackNtest/distributed/pdx/STU_files
+    >>> git fetch nyc
+    >>> git merge nyc/master
+    >>> ls shared/135.mae
+    shared/135.mae
+    >>> git annex whereis shared/135.mae
+    whereis shared/135.mae (1 copy)
+            6f0368db-f1b1-4192-9200-3575c16c2ef1 -- origin (nyc)
+    ok
+    >>> git annex get shared/135.mae
+    fatal: Could not switch to '../.git/annex/objects/KV/5f/SHA256-s1499628--4a7e2ba13096ee2d1a6b3c3b314efae623516d200c09d35ff0f695395b6ad47a': No such file or directory
+
+    git-annex: <file descriptor: 4>: hGetLine: end of file
+    failed
+    git-annex: get: 1 failed
+
+**What is the expected output? What do you see instead?**
+
+I expected the file shared/135.mae to be copied from the remote repo to the local repo.  Instead, this command failed, and said that there was a missing file.  This file path is the one that the broken link points to, and it exists on the remote repo.
+
+**What version of git-annex are you using? On what operating system?**
+
+git version 1.7.9.6
+
+git-annex 3.20120523
+
+CentOS 6.3 (kernel 2.6.32)
+
+64bit Xeon processor
+
+
+**Please provide any additional information below.**
+
+> Thanks for the command sequence, which I have tested here is ok with
+> a current version of git-annex (except for one cd you left out..).
+> 
+> You version of git-annex is quite old, and this
+> particular bug was fixed in version 3.20120721.
+> 
+> The bug is that it fails to correctly determine the git version at
+> compile time, and I think it thinks you have an old version of git
+> from before 1.7.7, which changed some behavior of `git check-attr`.
+> 
+> Upgrading git-annex should fix this, please let me know if not. [[done]]
+> --[[Joey]] 
diff --git a/doc/bugs/assistant_does_not_list_remote___39__origin__39__.mdwn b/doc/bugs/assistant_does_not_list_remote___39__origin__39__.mdwn
--- a/doc/bugs/assistant_does_not_list_remote___39__origin__39__.mdwn
+++ b/doc/bugs/assistant_does_not_list_remote___39__origin__39__.mdwn
@@ -20,3 +20,5 @@
 I tried both with direct and indirect mode for the local annex repo.
 
 I am sorry if I am missing the point. I checked the docs, however without much success.
+
+[[!tag /design/assistant]]
diff --git a/doc/bugs/assistant_ignore_.gitignore.mdwn b/doc/bugs/assistant_ignore_.gitignore.mdwn
--- a/doc/bugs/assistant_ignore_.gitignore.mdwn
+++ b/doc/bugs/assistant_ignore_.gitignore.mdwn
@@ -25,3 +25,5 @@
 > As noted in [[design/assistant/inotify]]'s TODO list, this
 > needs an efficient gitignore query interface in git (DNE) 
 > or a gitignore parser. --[[Joey]] 
+
+[[!tag /design/assistant]]
diff --git a/doc/bugs/encryption_key_is_surprising.mdwn b/doc/bugs/encryption_key_is_surprising.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/encryption_key_is_surprising.mdwn
@@ -0,0 +1,24 @@
+Crypto.hs seems to imply that the cipher key it's acting upon is 512 bytes long. Because of a probable programming mistake the key that's actually used is a bit surprising. The random key is generated by this snippet in Utility/Gpg.hs:
+
+    {- Creates a block of high-quality random data suitable to use as a cipher.
+     - It is armored, to avoid newlines, since gpg only reads ciphers up to the
+     - first newline. -}
+    genRandom :: Int -> IO String
+    genRandom size = readStrict
+        [ Params "--gen-random --armor"
+        , Param $ show randomquality
+        , Param $ show size
+        ]
+      where
+        -- 1 is /dev/urandom; 2 is /dev/random
+        randomquality = 1 :: Int
+
+This lets GPG generate the randomness and by passing armor, it avoids newlines. However, this base64 encoding is never undone on the way to Crypto.hs. Hence what cipherPassphrase and cipherHmac do is dropping or skipping the first 256 bytes of the base64 value. Base64, with its 6 bit per byte encoding, causes the Hmac function to operate on 192 bytes instead of 256 bytes. The key used by GPG will be larger. Some assertions that the resulting functions really operate on strings of the right length would've been helpful. Also GPG and HMAC get tested, the encryption/decryption part are not tested AFAICS.
+
+The encryption wiki page could have had more information. Enough code (sadly in Python, not reusing the Haskell code) to operate on the resulting files can be found in [this Gist](https://gist.github.com/pkern/5078559).
+
+-- Philipp Kern
+
+> In addition to the comment below, I have added a check that gpg outputs
+> the expected quantity of data, and the storage of the cipher is now
+> documented in [[internals]]. Think I can call this [[done]]. --[[Joey]]
diff --git a/doc/bugs/git-annex_dropunused_has_no_effect.mdwn b/doc/bugs/git-annex_dropunused_has_no_effect.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/git-annex_dropunused_has_no_effect.mdwn
@@ -0,0 +1,10 @@
+Hi Joey,
+
+I have a repository with many thousands of unused files.  It's hard to know exactly how many because it takes up to 5 seconds to print the name of every single one, so I'm largely guessing based on my knowledge of what I've recently deleted.
+
+When I run `git annex dropunused FOO`, it doesn't matter what `FOO` is -- a number, a range, the word "foo" -- the `dropunused` command returns to the prompt instantly in all cases.
+
+What can I do to drop all these unused files eating up i-nodes?  Is there a debug flag I can turn on?
+
+Thanks,
+  John
diff --git a/doc/bugs/host_with_rysnc_installed__44___not_recognized.txt b/doc/bugs/host_with_rysnc_installed__44___not_recognized.txt
new file mode 100644
--- /dev/null
+++ b/doc/bugs/host_with_rysnc_installed__44___not_recognized.txt
@@ -0,0 +1,12 @@
+What steps will reproduce the problem?
+Set up a remote server using ssh to a FreeNAS box
+
+What is the expected output? What do you see instead?
+Neither rsync nor git-annex are installed
+rsync is in the path, user with permissions is able to run it
+
+What version of git-annex are you using? On what operating system?
+4.20130227 OSX
+
+Please provide any additional information below.
+ssh keys were installed to allow login, when ssh-askpass was not found on osx version
diff --git a/doc/bugs/long_running_assistant_causes_resource_starvation_on_OSX.mdwn b/doc/bugs/long_running_assistant_causes_resource_starvation_on_OSX.mdwn
--- a/doc/bugs/long_running_assistant_causes_resource_starvation_on_OSX.mdwn
+++ b/doc/bugs/long_running_assistant_causes_resource_starvation_on_OSX.mdwn
@@ -22,3 +22,5 @@
 Please provide any additional information below.
 
 I'm really not sure what to look for next. Happy to take suggestions.
+
+[!tag /design/assistant]]
diff --git a/doc/bugs/random_files_vanishing_when_assistant_gets_restarted.mdwn b/doc/bugs/random_files_vanishing_when_assistant_gets_restarted.mdwn
--- a/doc/bugs/random_files_vanishing_when_assistant_gets_restarted.mdwn
+++ b/doc/bugs/random_files_vanishing_when_assistant_gets_restarted.mdwn
@@ -22,9 +22,13 @@
     
     Already up-to-date.
 
-Sorry for the german language, I'll try to reproduce it in english, later. After that, the symlinks for the file in the repository are gone. I can get them back by reverting the commit but things like that make me very nervous.
+Sorry for the german language, I'll try to reproduce it in english, later.
+After that, the symlinks for the file in the repository are gone. I can get
+them back by reverting the commit but things like that make me very nervous.
 
 
 #What version of git-annex are you using? On what operating system?
 
 3.20130102 on Arch Linux x64
+
+[!tag /design/assistant]]
diff --git a/doc/bugs/smarter_flood_filling.mdwn b/doc/bugs/smarter_flood_filling.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/smarter_flood_filling.mdwn
@@ -0,0 +1,31 @@
+The assistant performs a flood fill, sending every file to every remote
+that will have it. This is naive, but it's a good way to ensure the file
+gets to every corner of the repo network that it possibly can.
+
+However, this means that locally paired computers will still upload files
+to a transfer repo, even when they're next to each other and that
+is a massive waste of bandwidth.
+
+It occurred to me this morning that there is a simple change that can avoid
+this.
+
+1. Ensure that locally paired computers have a lower cost than network
+   transfer remotes. (done)
+2. When queuing uploads, queue transfers to the lowest cost remotes first.
+   (already done)
+3. Just before starting a transfer, re-check if the transfer is still wanted.
+   (done)
+
+> [[done]]
+
+Now, unnecessary transfers to tranfer repos are avoided if it can send
+the file locally instead.
+
+It doesn't solve it for all network topologies of course. If there
+are three computers paired in a line "A --- B --- C", and all 3 share
+a transfer repo, A will still send to both B and the transfer repo
+even though B can reach C via a faster route.
+
+See also: [[assistant does not always use repo cost info when queueing downloads]]
+
+[[!tag /design/assistant]]
diff --git a/doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform.mdwn b/doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform.mdwn
--- a/doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform.mdwn
+++ b/doc/bugs/tests_failed_to_build_-_after_an_update_of_haskell_platform.mdwn
@@ -18,3 +18,6 @@
 </pre>
 
 Looks like a missing dep somewhere with testpack or quickcheck... I haven't had time to figure it out yet, its not git-annex specific but I thought I might log it as a reminder for myself just in case if the osxapp is more borked than usual, I probably need to flush my .cabal directory of installed userland dependancies.
+
+> The testpack library has been broken by some other library changes. I
+> made changes in git yesterday to avoid using it. [[done]] --[[Joey]]
diff --git a/doc/bugs/three_way_sync_via_S3_and_Jabber.mdwn b/doc/bugs/three_way_sync_via_S3_and_Jabber.mdwn
--- a/doc/bugs/three_way_sync_via_S3_and_Jabber.mdwn
+++ b/doc/bugs/three_way_sync_via_S3_and_Jabber.mdwn
@@ -115,3 +115,5 @@
 
 This issue also made me wonder about how one would go about syncing multiple unrelated annexes via XMPP. Would you need a different gmail account for each? Maybe there is a trick similar to the email local+foo@ thing?
 
+> [[done]], turned out I left XMPP git push working,
+> but had not done all the stuff around it to get reliable syncing. Now have. --[[Joey]]
diff --git a/doc/bugs/unannex_removes_object_even_if_referred_to_by_others.mdwn b/doc/bugs/unannex_removes_object_even_if_referred_to_by_others.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/unannex_removes_object_even_if_referred_to_by_others.mdwn
@@ -0,0 +1,18 @@
+##What steps will reproduce the problem?
+
+    echo text > foo
+    echo text > bar
+    git annex add foo bar
+    git annex unannex foo
+
+##What is the expected output? What do you see instead?
+
+I would expect that the object behind 'bar' remained intact, what happens is that the object is moved out of the annex and 'bar' is left as a dangling symlink, if you are unlucky and don't spot this, it could be potentially dangerous, since you can easily lose data.
+
+##What version of git-annex are you using? On what operating system?
+
+git-annex built from git on Tue Mar 12 15:58:36 2013 -0400
+
+From commit: 70b7555eaf9ac5f88bb137985d93bed8d5a434e8
+
+On Debian Sid
diff --git a/doc/bugs/webapp_hang.mdwn b/doc/bugs/webapp_hang.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/bugs/webapp_hang.mdwn
@@ -0,0 +1,141 @@
+Occasionally, clicking on a link in the webapp will hang. When this
+happens, which has only been seen in Chromium so far, the tab will spin
+forever, without anything loading. Other tabs can be opened with
+middle-click on links in the webapp, and work fine. Stopping and reloading
+in the tab tends to hang again, although eventually this will clear the
+hang up.
+
+-------
+
+My best procedure to replicate this, about 25% of the time:
+
+* have 2 large files and a libreoffice spreadsheet
+* start webapp, make repository
+* open file browser using button in webapp
+* move files into repository
+* make media subdir; move media into it
+* open spreadsheet, modify, save
+* click on New Repository button for the hang
+
+Running recordmydesktop at the same time may also help.. Or giving a
+presentation in Australia. :/
+
+-------
+
+Hypotheses:
+
+* Warp's slowloris protection could be triggering. Possibly by the
+  repeated hits to update the alerts? I added a settingsOnException handler
+  that logs all exceptions, and ThreadKilled is happening several times.
+  The only place in Warp that kills threads is due to a timeout installed
+  for that. 
+
+  **Verified** Bug filed upstream:  <https://github.com/yesodweb/wai/issues/146>
+
+  ** workaround in place **
+
+* Something deep in git-annex, such as the inotidy code, could be
+  preventing a web server thread from running. But then why do other
+  tabs and other web browsers work while it's stuck?
+  It would have to affect only 1 thread.
+
+-------
+
+I captured a clean protocol dump of this happening. It includes only the
+final, hanging http request and subsequent traffic, not the setup.
+
+We can see the web browser send a request. The server ACKs it at the TCP
+level, but never sends any reply. The web browser continues sending TCP
+keep-alive packets, which are acked by the server. This continued as long
+as the browser tab was left open.
+
+Question: Did the browser send a complete & valid request? The last part
+sent is a cookie and "\r\n\r\n".. So it seems complete. Unless warp is
+expecting a request body?
+
+<pre>
+17:22:30.533079 IP localhost.localdomain.53239 > localhost.localdomain.45836: Flags [P.], seq 4237976899:4237977772, ack 2608808926, win 2048, options [nop,nop,TS val 4895706 ecr 4885760], length 873
+	0x0000:  4500 039d be84 4000 4006 7ad4 7f00 0001  E.....@.@.z.....
+	0x0010:  7f00 0001 cff7 b30c fc9a 6543 9b7f 43de  ..........eC..C.
+	0x0020:  8018 0800 0192 0000 0101 080a 004a b3da  .............J..
+	0x0030:  004a 8d00 4745 5420 2f63 6f6e 6669 672f  .J..GET./config/
+	0x0040:  7265 706f 7369 746f 7279 3f61 7574 683d  repository?auth=
+	0x0050:  6437 6266 3037 3438 6663 3863 3031 3965  d7bf0748fc8c019e
+	0x0060:  6230 3966 3530 3631 6164 6663 3861 3563  b09f5061adfc8a5c
+	0x0070:  3430 3437 3633 6562 3736 6630 6163 3533  404763eb76f0ac53
+	0x0080:  3663 3362 6230 3066 3835 6663 6361 3233  6c3bb00f85fcca23
+	0x0090:  6235 3639 3764 3332 3464 3737 3930 3063  b5697d324d77900c
+	0x00a0:  3739 3532 6430 6165 3235 3166 6331 6337  7952d0ae251fc1c7
+	0x00b0:  3532 3632 6330 3233 6265 3936 3066 3563  5262c023be960f5c
+	0x00c0:  3364 6135 6532 6262 6234 3639 3863 3035  3da5e2bbb4698c05
+	0x00d0:  2048 5454 502f 312e 310d 0a48 6f73 743a  .HTTP/1.1..Host:
+	0x00e0:  2031 3237 2e30 2e30 2e31 3a34 3538 3336  .127.0.0.1:45836
+	0x00f0:  0d0a 436f 6e6e 6563 7469 6f6e 3a20 6b65  ..Connection:.ke
+	0x0100:  6570 2d61 6c69 7665 0d0a 4163 6365 7074  ep-alive..Accept
+	0x0110:  3a20 7465 7874 2f68 746d 6c2c 6170 706c  :.text/html,appl
+	0x0120:  6963 6174 696f 6e2f 7868 746d 6c2b 786d  ication/xhtml+xm
+	0x0130:  6c2c 6170 706c 6963 6174 696f 6e2f 786d  l,application/xm
+	0x0140:  6c3b 713d 302e 392c 2a2f 2a3b 713d 302e  l;q=0.9,*/*;q=0.
+	0x0150:  380d 0a55 7365 722d 4167 656e 743a 204d  8..User-Agent:.M
+	0x0160:  6f7a 696c 6c61 2f35 2e30 2028 5831 313b  ozilla/5.0.(X11;
+	0x0170:  204c 696e 7578 2069 3638 3629 2041 7070  .Linux.i686).App
+	0x0180:  6c65 5765 624b 6974 2f35 3337 2e31 3720  leWebKit/537.17.
+	0x0190:  284b 4854 4d4c 2c20 6c69 6b65 2047 6563  (KHTML,.like.Gec
+	0x01a0:  6b6f 2920 4368 726f 6d65 2f32 342e 302e  ko).Chrome/24.0.
+	0x01b0:  3133 3132 2e36 3820 5361 6661 7269 2f35  1312.68.Safari/5
+	0x01c0:  3337 2e31 370d 0a52 6566 6572 6572 3a20  37.17..Referer:.
+	0x01d0:  6874 7470 3a2f 2f31 3237 2e30 2e30 2e31  http://127.0.0.1
+	0x01e0:  3a34 3538 3336 2f3f 6175 7468 3d64 3762  :45836/?auth=d7b
+	0x01f0:  6630 3734 3866 6338 6330 3139 6562 3039  f0748fc8c019eb09
+	0x0200:  6635 3036 3161 6466 6338 6135 6334 3034  f5061adfc8a5c404
+	0x0210:  3736 3365 6237 3666 3061 6335 3336 6333  763eb76f0ac536c3
+	0x0220:  6262 3030 6638 3566 6363 6132 3362 3536  bb00f85fcca23b56
+	0x0230:  3937 6433 3234 6437 3739 3030 6337 3935  97d324d77900c795
+	0x0240:  3264 3061 6532 3531 6663 3163 3735 3236  2d0ae251fc1c7526
+	0x0250:  3263 3032 3362 6539 3630 6635 6333 6461  2c023be960f5c3da
+	0x0260:  3565 3262 6262 3436 3938 6330 350d 0a41  5e2bbb4698c05..A
+	0x0270:  6363 6570 742d 456e 636f 6469 6e67 3a20  ccept-Encoding:.
+	0x0280:  677a 6970 2c64 6566 6c61 7465 2c73 6463  gzip,deflate,sdc
+	0x0290:  680d 0a41 6363 6570 742d 4c61 6e67 7561  h..Accept-Langua
+	0x02a0:  6765 3a20 656e 2d55 532c 656e 3b71 3d30  ge:.en-US,en;q=0
+	0x02b0:  2e38 0d0a 4163 6365 7074 2d43 6861 7273  .8..Accept-Chars
+	0x02c0:  6574 3a20 4953 4f2d 3838 3539 2d31 2c75  et:.ISO-8859-1,u
+	0x02d0:  7466 2d38 3b71 3d30 2e37 2c2a 3b71 3d30  tf-8;q=0.7,*;q=0
+	0x02e0:  2e33 0d0a 436f 6f6b 6965 3a20 5f53 4553  .3..Cookie:._SES
+	0x02f0:  5349 4f4e 3d45 3363 7455 496c 7341 5451  SION=E3ctUIlsATQ
+	0x0300:  3631 694c 6d54 6954 7131 6f37 6465 7830  61iLmTiTq1o7dex0
+	0x0310:  3361 6f57 3049 4b63 7663 467a 5838 4344  3aoW0IKcvcFzX8CD
+	0x0320:  5432 7666 4b31 6c42 416d 6279 3166 764f  T2vfK1lBAmby1fvO
+	0x0330:  4643 7952 7863 6f5a 6277 5633 6a4b 4971  FCyRxcoZbwV3jKIq
+	0x0340:  6b35 6958 4674 7557 5261 6b48 6944 6132  k5iXFtuWRakHiDa2
+	0x0350:  7769 3075 2f53 6430 5a49 7a75 4464 7947  wi0u/Sd0ZIzuDdyG
+	0x0360:  774f 6a31 7838 3130 356a 772f 5a2b 355a  wOj1x8105jw/Z+5Z
+	0x0370:  6f4b 6f48 396e 6779 6e4b 5366 5839 742f  oKoH9ngynKSfX9t/
+	0x0380:  6862 4b79 435a 6966 7739 4148 3053 6d4b  hbKyCZifw9AH0SmK
+	0x0390:  436e 4c38 5358 513d 3d0d 0a0d 0a         CnL8SXQ==....
+17:22:30.571152 IP localhost.localdomain.45836 > localhost.localdomain.53239: Flags [.], ack 873, win 2048, options [nop,nop,TS val 4895716 ecr 4895706], length 0
+	0x0000:  4500 0034 f15b 4000 4006 4b66 7f00 0001  E..4.[@.@.Kf....
+	0x0010:  7f00 0001 b30c cff7 9b7f 43de fc9a 68ac  ..........C...h.
+	0x0020:  8010 0800 fe28 0000 0101 080a 004a b3e4  .....(.......J..
+	0x0030:  004a b3da                                .J..
+17:22:35.803152 IP localhost.localdomain.53240 > localhost.localdomain.45836: Flags [.], ack 2157632553, win 2048, options [nop,nop,TS val 4897024 ecr 4885760], length 0
+	0x0000:  4500 0034 3a63 4000 4006 025f 7f00 0001  E..4:c@.@.._....
+	0x0010:  7f00 0001 cff8 b30c 7533 e963 809a dc29  ........u3.c...)
+	0x0020:  8010 0800 fe28 0000 0101 080a 004a b900  .....(.......J..
+	0x0030:  004a 8d00                                .J..
+17:22:35.803213 IP localhost.localdomain.45836 > localhost.localdomain.53240: Flags [.], ack 1, win 2048, options [nop,nop,TS val 4897024 ecr 4840696], length 0
+	0x0000:  4500 0034 10e5 4000 4006 2bdd 7f00 0001  E..4..@.@.+.....
+	0x0010:  7f00 0001 b30c cff8 809a dc29 7533 e964  ...........)u3.d
+	0x0020:  8010 0800 fe28 0000 0101 080a 004a b900  .....(.......J..
+	0x0030:  0049 dcf8                                .I..
+17:23:15.611193 IP localhost.localdomain.53239 > localhost.localdomain.45836: Flags [.], ack 1, win 2048, options [nop,nop,TS val 4906976 ecr 4895716], length 0
+	0x0000:  4500 0034 be85 4000 4006 7e3c 7f00 0001  E..4..@.@.~<....
+	0x0010:  7f00 0001 cff7 b30c fc9a 68ab 9b7f 43de  ..........h...C.
+	0x0020:  8010 0800 fe28 0000 0101 080a 004a dfe0  .....(.......J..
+	0x0030:  004a b3e4                                .J..
+17:23:15.611290 IP localhost.localdomain.45836 > localhost.localdomain.53239: Flags [.], ack 873, win 2048, options [nop,nop,TS val 4906976 ecr 4895706], length 0
+	0x0000:  4500 0034 f15c 4000 4006 4b65 7f00 0001  E..4.\@.@.Ke....
+	0x0010:  7f00 0001 b30c cff7 9b7f 43de fc9a 68ac  ..........C...h.
+	0x0020:  8010 0800 fe28 0000 0101 080a 004a dfe0  .....(.......J..
+	0x0030:  004a b3da                                .J..
+</pre>
diff --git a/doc/bugs/webapp_hang/comment_1_08aa908a64d0fe2d50438d01545c3f01._comment b/doc/bugs/webapp_hang/comment_1_08aa908a64d0fe2d50438d01545c3f01._comment
new file mode 100644
--- /dev/null
+++ b/doc/bugs/webapp_hang/comment_1_08aa908a64d0fe2d50438d01545c3f01._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="http://gdr.name/"
+ nickname="gdr.name"
+ subject="Not only Chrome"
+ date="2013-03-09T10:55:57Z"
+ content="""
+It happened to me numerous times with Opera. I was never able to repeat it hence no bug report.
+"""]]
diff --git a/doc/bugs/webapp_hang/comment_2_2a21ac5657128a454f9deb77c4d18057._comment b/doc/bugs/webapp_hang/comment_2_2a21ac5657128a454f9deb77c4d18057._comment
new file mode 100644
--- /dev/null
+++ b/doc/bugs/webapp_hang/comment_2_2a21ac5657128a454f9deb77c4d18057._comment
@@ -0,0 +1,21 @@
+[[!comment format=mdwn
+ username="http://crosstwine.com/dd/"
+ ip="88.65.128.43"
+ subject="chrome://net-internals/"
+ date="2013-03-09T20:10:54Z"
+ content="""
+Hi Joey,
+
+I see that you have found the cause and a workaround for this particular
+problem, but would like to point out that `chrome://net-internals/` can be
+very useful for diagnosing such issues.
+
+(I once hit the `SOCKET_POOL_STALLED_MAX_SOCKETS_PER_GROUP` problem
+mentioned in
+<https://code.google.com/p/chromium/issues/detail?id=27324>, which can
+cause Chromium to keep spinning while not issuing any new requests to a
+specific web server.)
+
+Cheers,  
+Damien Diederen
+"""]]
diff --git a/doc/design/assistant.mdwn b/doc/design/assistant.mdwn
--- a/doc/design/assistant.mdwn
+++ b/doc/design/assistant.mdwn
@@ -29,6 +29,7 @@
 ## not yet on the map:
 
 * [[partial_content]]
+* encrypted git remotes using [git-remote-gcrypt](https://github.com/blake2-ppc/git-remote-gcrypt)
 * [[deltas]]
 * [[leftovers]]
 * [[other todo items|todo]]
diff --git a/doc/design/assistant/android.mdwn b/doc/design/assistant/android.mdwn
--- a/doc/design/assistant/android.mdwn
+++ b/doc/design/assistant/android.mdwn
@@ -5,7 +5,10 @@
 2. Deal with crippled filesystem; no symlinks; etc. **done**
 3. Get an easy to install Android app built. **done**
 4. Get the webapp working. Needs Template Haskell, or 
-   switching to <http://www.yesodweb.com/blog/2012/10/yesod-pure>
+   switching to <http://www.yesodweb.com/blog/2012/10/yesod-pure>.
+5. Possibly, switch from running inside terminal app to real standalone app.
+   See <https://github.com/neurocyte/android-haskell-activity>
+   and <https://github.com/neurocyte/foreign-jni>.
 
 ### Android specific features
 
@@ -15,6 +18,12 @@
 The app should be aware of network status, and avoid expensive data
 transfers when not on wifi. This may need to be configurable.
 
+## Template Haskell for android?
+
+Best lead I have on getting cross compilation of TH working is that GHCJS
+does it, and that it involves compiling each file twice, once natively for
+TH and once for cross.
+
 ## TODO
 
 * webapp
@@ -33,3 +42,5 @@
 * Enable WebDAV support. Currently needs template haskell (could be avoided
   by changing the DAV library to not use it), and also networking support,
   which seems broken in current ghc-android.
+* Get test suite to pass. Current failure is because `git fetch` is somehow
+  broken with local repositories.
diff --git a/doc/design/assistant/blog/day_201__real_Android_wrapup.mdwn b/doc/design/assistant/blog/day_201__real_Android_wrapup.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_201__real_Android_wrapup.mdwn
@@ -0,0 +1,38 @@
+[[!meta title="day 201  real Android wrapup"]]
+
+I got yesod-pure fully working on Android...
+
+[[!img fib.png size=400x]]
+
+As expected, this involved manually splicing some template haskell. I'm now
+confident I can port the git-annex webapp to Android this way, and that it
+will take about a week. Probably will start on that in a month or so. If
+anyone has some spare Android hardware they'd like to loan me, possibly
+sooner. (Returning loaner Asus Transformer tomorrow; thanks Mark.) Although
+I'm inclined to let the situation develop; we may just get a ghc-android
+that supports TH..
+
+Also:
+
+* Fixed several bugs in the Android installation process.
+* Committed patches for all Haskell libraries I've modified to
+  the git-annex git repo.
+* Ran the test suite on Android. It found a problem; seems `git clone`
+  of a local repository is broken in the Android environment.
+
+Non-Android:
+
+* Made the assistant check every hour if logs have grown larger than a
+  megabyte, and rotate them to avoid using too much disk space.
+* Avoided noise in log about typechanged objects when running
+  git commit in direct mode repositories. Seems `git commit`
+  has no way to shut that up, so I had to /dev/null it.
+* When run with `--debug`, the assistant now logs more information
+  about why it transfers or drops objects.
+* Found and fixed a case where moving a file to an archive directory would
+  not cause its content to be dropped.
+* Working on a bug with the assistant where moving a file out of an 
+  archive directory in direct mode sometimes ends up with a symlink
+  rather than a proper direct mode file. Have not gotten to the bottom
+  of it entirely, but it's a race, and I think the race is between
+  the direct mode mapping being updated, and the file being transferred.
diff --git a/doc/design/assistant/blog/day_201__real_Android_wrapup/fib.png b/doc/design/assistant/blog/day_201__real_Android_wrapup/fib.png
new file mode 100644
Binary files /dev/null and b/doc/design/assistant/blog/day_201__real_Android_wrapup/fib.png differ
diff --git a/doc/design/assistant/blog/day_201__working_web_server.mdwn b/doc/design/assistant/blog/day_201__working_web_server.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_201__working_web_server.mdwn
@@ -0,0 +1,31 @@
+Seems I am not done with the Android porting just yet after all. One more
+porting day..
+
+Last night I managed to get all of Yesod to build for Android.
+I even successfully expanded some Template Haskell used in yesod-form. And
+am fairly confident I could manually expand all the TH in there, so it's
+actually useable without TH. Most of the TH is just commented out for now.
+
+However, programs using Yesod didn't link; lots of missing symbols. I have
+been fighting to fix those all day today.
+
+Finally, I managed to build [the yesod-pure demo server](https://gist.github.com/snoyberg/3870834/raw/212f0164de36524291df3ab35788e2b72d8d1e75/fib.hs),
+and I have a working web server on Android! It listens for requests, it logs
+them correctly, and it replies to requests. I did cripple yesod's routing
+code in my hack-n-slash port of it, so it fails to *display* any pages,
+but never has "Internal Server Error" in a web browser been such a sweet
+sight. ;-)
+
+At this point, I estimate about 1 or 2 weeks work to get to an Android
+webapp. I'd need to:
+
+1. More carefully port Yesod, manually expanding all Template Haskell
+   as I went, rather than commenting it all out like I did this time.
+2. Either develop a tool to automatically expand Hamlet TH splices
+   (preferred; seems doable), or convert all the webapp's templates
+   to not use Hamlet.
+
+-----
+
+I've modified 38 Haskell libraries so far to port them to Android. Mostly
+small hacks, but eep this is a lot of stuff to keep straight.
diff --git a/doc/design/assistant/blog/day_203__procrastination.mdwn b/doc/design/assistant/blog/day_203__procrastination.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_203__procrastination.mdwn
@@ -0,0 +1,25 @@
+Stuck on a bug or two, I instead built a new Preferences page:
+
+[[!img /assistant/preferences.png]]
+
+The main reason I wanted that was to allow enabling debug logging at
+runtime. But I've also wanted to expose annex.diskreserve and
+annex.numcopies settings to the webapp user. Might as well let them control
+whether it auto-starts too.
+
+Had some difficulty deciding where to put this. It could be considered
+additional configuration for the local repository, and so go in the
+local repository edit form. However, this stuff can only be configured for
+local repositories, and not remotes, and that same form is used to edit 
+remotes, which would lead to inconsistent UI and complicate the code.
+Also, it might grow to include things not tied to any repository,
+like choice of key/value backends. So, I put the preferences on their own
+page.
+
+---
+
+Also, based on some useful feedback from testing the assistant with a large
+number of files, I made the assistant disable git-gc auto packing in
+repositories it sets up. (Like fsck, git-gc always seems to run exactly
+when you are in a hurry.) Instead, it'll pack at most once a day, and with
+a rather higher threshold for the number of loose objects.
diff --git a/doc/design/assistant/blog/day_204__deprocrastination.mdwn b/doc/design/assistant/blog/day_204__deprocrastination.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_204__deprocrastination.mdwn
@@ -0,0 +1,62 @@
+Tracked down the bug that's been eluding me for days. It was indeed a race, and
+could result in a file being transferred into a direct mode repository and
+ending up in indirect mode. Was easy to fix once understood, just needed to
+update the direct mode mapping before starting the transfer.
+
+While I was in there, I noticed another potential race, also in direct
+mode, where the watcher could decide to rewrite a symlink to fix its
+target, and at just the wrong time direct mode content could arrive in its
+place, and so get deleted. Fixed that too.
+
+Seems likely there are some other direct mode races. I spent quite a while
+hammering on dealing with the indirect mode races with the assistant 
+originally.
+
+-----
+
+Next on my list is revisiting XMPP.
+
+Verified that git push over XMPP works between multiple repositories that
+are sharing the same XMPP account. It does.
+
+Seeing the XMPP setup process with fresh eyes, I found several places
+wording could be improved. Also, when the user goes in and configures 
+(or reconfigures) an XMPP account, the next step is to do pairing,
+so it now redirects directly to there.
+
+Next I need to make XMPP get back into sync after a network disconnection
+or when the assistant is restarted. This currently doesn't happen until
+a XMPP push is received due to a new change being made.
+
+### back burner: yesod-pure
+
+Last night I made a yesod-pure branch, and did some exploratory conversion
+away from using Hamlet, of the Preferences page I built yesterday.
+
+I was actually finding writing pure Blaze worked *better* than Hamlet,
+at first. Was able to refactor several things into functions that in Hamlet
+are duplicated over and over in my templates, and built some stuff that makes
+rendering type safe urls in pure Blaze not particularly ungainly. For example,
+this makes a submit button and a cancel button that redirects to another page:
+
+[[!format haskell """
+        buttons = toWidget $ \redir ->
+               "Save Preferences" <>|<> redir ConfigurationR []
+"""]]
+
+The catch is how to deal with widgets that need to be nested inside other
+html. It's not possible to do this both cleanly and maximally
+efficiently, with Blaze. For max efficiency, all the html before the widget
+should be emitted, and then the widget run, and then all html after it be
+emitted. To use Blaze, it would have to instead generate the full html,
+then split it around the widget, and then emit the parts, which is less
+efficient, doesn't stream, etc.
+
+I guess that's the core of what Hamlet does; it allows a clean
+representation and due to running TH at build time, can convert this into
+an efficient (but ugly) html emitter.
+
+So, I may give up on this experiment. Or I may make the webapp less than
+maximally fast at generating html and go on with it. After all, these
+sorts of optimisations are mostly aimed at high-volume websites, not local
+webapps.
diff --git a/doc/design/assistant/blog/day_205_206__rainy_day__snow_day.mdwn b/doc/design/assistant/blog/day_205_206__rainy_day__snow_day.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_205_206__rainy_day__snow_day.mdwn
@@ -0,0 +1,12 @@
+Yesterday was all bug fixes, nothing to write about really.
+
+Today I've been working on getting XMPP remotes to sync more reliably.
+I left some big holes when I stopped work on it in November:
+
+1. The assistant did not sync with XMPP remotes when it started up.
+2. .. Or when it detected a network reconnection.
+3. There was no way to trigger a full scan for transfers
+   after receiving a push from an XMPP remote.
+
+The asynchronous nature of git push over XMPP complicated doing this, but
+I've solved all 3 issues today.
diff --git a/doc/design/assistant/blog/day_207__XMPP.mdwn b/doc/design/assistant/blog/day_207__XMPP.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_207__XMPP.mdwn
@@ -0,0 +1,7 @@
+More XMPP fixes. The most important change is that it now stores important
+messages, like push requests, and (re)sends them when a buddy's client
+sends XMPP presence. This makes XMPP syncing much more robust, all the
+clients do not need to already be connected when messages are initially
+sent, but can come and go. Also fixed a bug preventing syncing from working
+immediately after XMPP pairing. XMPP seems to be working well now; I only
+know of one minor bug.
diff --git a/doc/design/assistant/blog/day_208__bugfixes.mdwn b/doc/design/assistant/blog/day_208__bugfixes.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_208__bugfixes.mdwn
@@ -0,0 +1,17 @@
+Fixed the last XMPP bug I know of. Turns out it was not specific to XMPP at
+all; the assistant could forget to sync with any repository on startup
+under certain conditions.
+
+Also fixed bugs in `git annex add` and in the glob matching, and some more.
+
+I've been working on some screencasts. More on them later.. But while doing
+them I found a perfect way to reliably reproduce the webapp hang that
+I've been chasing for half a year, and last saw at my presentation in
+Australia. Seems the old joke about bugs only reproducible during
+presentations is literally true here!
+
+I have given this bug its [[own page|bugs/webapp_hang]] at last, and have a
+tcpdump of it happening and everything. Am working on an hypotheses that it
+might be caused by Warp's [slowloris](http://ha.ckers.org/slowloris/)
+attack prevention code being falsely triggered by the repeated hits the web
+browser makes as the webapp's display is updated.
diff --git a/doc/design/assistant/blog/day_209__The_Bug.mdwn b/doc/design/assistant/blog/day_209__The_Bug.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_209__The_Bug.mdwn
@@ -0,0 +1,23 @@
+> And so we waited. Tick-tock, blink-blink, thirty seconds stretched
+> themselves out one by one, a hole in human experience. -- The Bug
+
+I *think* I've managed to fully track down the [[webapp_hang]]. It is,
+apparently, a bug in the Warp web server's code intended to protect against
+the [Slowloris](http://ha.ckers.org/slowloris/) attack. It assumes,
+incorrectly, that a web browser won't reuse a connection it's left idle for
+30 seconds. Some bad error handling keeps a connection open with no thread
+to service it, leading to the hang.
+<https://github.com/yesodweb/wai/issues/146>
+
+Have put a 30 minute timeout into place as a workaround, and, unless
+a web browser sits on an idle connection for a full 30 minutes and then
+tries to reuse it, this should be sufficient.
+
+I was chasing that bug, quietly, for 6 months. Would see it now and
+then, but not be able to reproduce it or get anywhere with analysis.
+I had nearly given up. If you enjoy stories like that, read Ellen
+Ullman's excellent book The Bug.
+
+> To discover that between the blinks of the machine’s shuttered eye—going
+> on without pause or cease; simulated, imagined, but still not caught—was
+> life.
diff --git a/doc/design/assistant/blog/day_210__spring.mdwn b/doc/design/assistant/blog/day_210__spring.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_210__spring.mdwn
@@ -0,0 +1,29 @@
+Trying to record screencasts demoing the assistant is really helping me
+see things that need to be fixed.
+
+Got the version of the haskell TLS library in Debian fixed, backporting
+some changes to fix a botched security fix that made it reject all
+certificates. So WebDAV special remotes will work again on the next release.
+
+Fixed some more problems around content being dropped when files are
+moved to archive directories, and gotten again when files are
+moved out.
+
+Fixed some problems around USB drives. One was a real jaw-dropping
+bug: "git annex drop --from usbdrive" when the drive was not
+connected still updated the location log to indicate it did not have
+the file anymore! (Thank goodness for fsck..)
+
+I've noticed that moving around files in direct mode repos is inneficient,
+because the assistant re-checksums the "new" file. One way to avoid
+that would be to have a lookup table from (inode, size, mtime) to
+key, but I don't have one, and would like to avoid adding one.
+
+Instead, I have a cunning plan to deal with this heuristically. If the
+assistant can notice a file was removed and another file added at the same
+time, it can compare the (inode, size, mtime) to see if it's a rename, and
+avoid the checksum overhead.
+
+The first step to getting there was to make the assistant better at
+batching together delete+add events into a single rename commit. I'm happy
+to say I've accomplished that, with no perceptable delay to commits.
diff --git a/doc/design/assistant/blog/day_211__zooming_along.mdwn b/doc/design/assistant/blog/day_211__zooming_along.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_211__zooming_along.mdwn
@@ -0,0 +1,24 @@
+Got renaming fully optimised in the assistent in direct mode. I even got it
+to work for whole directory renames. I can drag files around all day in the
+file manager and the assistant often finishes committing the rename before
+the file manager updates. So much better than checksumming every single
+renamed file! Also, this means the assistant makes just 1 commit when a
+whole directory is renamed.
+
+Last night I added a feature to `git annex status`. It can now be asked to
+only show the status of a single directory, rather than the whole annex.
+All the regular file filtering switches work, so some neat commands
+are possible. I like `git annex status . --in foo --not --in bar` to see
+how much data is in one remote but not another.
+
+This morning, an important thought about [[bugs/smarter_flood_filling]],
+that will avoid unnecessary uploads to transfer remotes when all that's
+needed to get the file to its destination is a transfer over the LAN.
+I found an easy way to make that work, at least in simple cases.
+Hoping to implement it soon.
+
+Less fun, direct mode turns out to be somewhat buggy when files with
+duplicate content are in the repository. Nothing fails, but `git annex
+sync` will re-checksum files each time it's run in this situation, and the
+assistant will re-checksum files in certian cases. Need to work on this
+soon too.
diff --git a/doc/design/assistant/blog/day_212__accidental_all_nighter.mdwn b/doc/design/assistant/blog/day_212__accidental_all_nighter.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_212__accidental_all_nighter.mdwn
@@ -0,0 +1,24 @@
+Last night, revamped the web site, including making a [[/videos]]
+page, which includes a new screencast introducing the git-annex assistant.
+
+Worked on improving my Haskell development environment in vim.
+hdevtools is an excellent but tricky thing to get working. Where before
+it took around 30 seconds per compile for me to see type errors,
+I now see them in under a second each time I save, and can also look up
+types of any expression in the file. Since programming in Haskell is
+mostly driven by reacting to type errors ;) this should speed me up a lot,
+although it's not perfect. Unfortunatly, I got really caught up in tuning
+my setup, and only finished doing that at 5:48 am.
+
+Disasterously late this morning, fixed the assistant's
+`~/.ssh/git-annex-shell` wrapper so it will work when the ssh key does
+not force a command to be run. Also made the webapp behave better
+when it's told to create a git repository that already exists.
+
+After entirely too little sleep, I found a puzzling bug where copying files
+to a local repo fails once the inode cache has been invalidated. This
+turned out to involve running a check in the state monad of the wrong
+repository. A failure mode I'd never encountered before.
+
+Only thing I had brains left to do today was to record another screencast,
+which is rendering now...
diff --git a/doc/design/assistant/blog/day_213__costs.mdwn b/doc/design/assistant/blog/day_213__costs.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_213__costs.mdwn
@@ -0,0 +1,34 @@
+Got the assistant to check again, just before starting a transfer, if
+the remote still wants the object. This should be all that's needed to
+handle the case where there is a transfer remote on the internet somewhere,
+and a locally paired client on the LAN. As long as the paired repository
+has a lower cost value, it will be sent any new file first, and if that
+is the only client, the file will not be sent to the transfer remote at
+all.
+
+But.. locally paired repos did not have a lower cost set, at all.
+So I made their cost be set to 175 when they're created. Anyone
+who already did local pairing should make sure the Repositories
+list shows locally paired repositories above transfer remotes.
+
+Which brought me to needing an easy way to reorder that list of remotes,
+which I plan to do by letting the user drag and drop remotes around,
+which will change their cost accordingly. Implementing that has two
+pain points:
+
+1. Often a lot of remotes will have the same default cost value.
+   So how to insert a remote in between two that have cost 100?
+   This would be easy if git-annex didn't have these cost numbers,
+   and instead just had an ordered list of remotes.. but it doesn't.
+   Instead, dragging remotes in the list will sometimes need to change
+   the costs of others, to make room to insert them in. It's BASIC
+   renumbering all over again. So I wrote some code to do this with as
+   little bother as possible.
+
+2. Drag and drop means javascript. I got the basics going quickly with
+   jquery-ui, only to get stuck for over an hour on some CSS issue
+   that made lines from the list display all weird while being dragged.
+   It is always something like this with javascript..
+
+So I've got these 2 peices working, and even have the AJAX call
+firing, but it's not quite wired up just yet. Tomorrow.
diff --git a/doc/design/assistant/blog/day_214__release_day.mdwn b/doc/design/assistant/blog/day_214__release_day.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/design/assistant/blog/day_214__release_day.mdwn
@@ -0,0 +1,5 @@
+Fighting with javascript all day and racing to get a release out. Unstuck
+the OSX and Android autobuilders. Got drag and drop repository list
+reordering working great. Tons of changes in this release!
+
+Also put up a new podcast.
diff --git a/doc/design/assistant/syncing.mdwn b/doc/design/assistant/syncing.mdwn
--- a/doc/design/assistant/syncing.mdwn
+++ b/doc/design/assistant/syncing.mdwn
@@ -3,13 +3,17 @@
 
 ## bugs
 
-* Running the assistant in a fresh clone of a repository, it sometimes
-  skips downloading a file, while successfully downloading all the rest.
-  There does not seem to be an error message. This will sometimes reproduce
-  (in a fresh clone each time) several times in a row, but then stops happening,
-  which has prevented me from debugging it.
-  This could possibly have been caused by the bug fixed in 750c4ac6c282d14d19f79e0711f858367da145e4.
+* Rename detection code is now pretty good, but can occasionally fail
+  due to eg system load causing the wait for the delete+add pair to
+  not collect them together. Want to avoid making the delay longer
+  (and any length might not be enough in all cases), but in direct mode
+  a failure can mean an expensive re-checksum.
 
+  Idea: Keep a ring buffer map (what data structure?) of the last
+  N delete events, and look through it to determine if the current
+  event matches one of those. This assumes that delete events come first,
+  which they do.
+
 ## TODO
 
 * Test MountWatcher on LXDE.
@@ -31,7 +35,8 @@
   bounced and the cached ssh connection not be usable.
 * Map the network of git repos, and use that map to calculate
   optimal transfers to keep the data in sync. Currently a naive flood fill
-  is done instead.
+  is done instead. Maybe use XMPP as a side channel to learn about the
+  network topology?
 * Find a more efficient way for the TransferScanner to find the transfers
   that need to be done to sync with a remote. Currently it walks the git
   working copy and checks each file. That probably needs to be done once,
@@ -247,3 +252,11 @@
   Note that this solution won't cover use cases the other does. For example,
   connect a USB drive A; B syncs files from it, and then should pass them to C.
   If the files are not new, C won't immediatly request them from B.
+
+* Running the assistant in a fresh clone of a repository, it sometimes
+  skips downloading a file, while successfully downloading all the rest.
+  There does not seem to be an error message. This will sometimes reproduce
+  (in a fresh clone each time) several times in a row, but then stops happening,
+  which has prevented me from debugging it.
+  This could possibly have been caused by the bug fixed in 750c4ac6c282d14d19f79e0711f858367da145e4.
+  Provisionally closed.
diff --git a/doc/design/assistant/webapp.mdwn b/doc/design/assistant/webapp.mdwn
--- a/doc/design/assistant/webapp.mdwn
+++ b/doc/design/assistant/webapp.mdwn
@@ -1,22 +1,14 @@
 The webapp is a web server that displays a shiny interface.
 
-## bugs
-
-* At least in chromium, clicking on the transfer pause or cancel button
-  sometimes fails. Seen in javascript console:
-  500 error code from web server.
-  This is quite likely because of how the div containing transfers is refereshed.
-  If instead javascript was used to update the progress bar etc for transfers
-  with json data, the buttons would work better.
-
 ## interface
 
-* list of files uploading and downloading **done**
-* button to open file browser on repo (`xdg-open $DIR`) **done**
-* progress bars for each file (see [[progressbars]]) **done**
+* Combine the replist with the dashboard. Put the list of repos or nudge
+  to make repos on top, and the transfers below. Make a "+ Add repo" button
+  on the list of repos that expands a hidden div, showing the repo creation
+  choices. Only one problem: If I have 20 repositories, all
+  I can see on the dashboard w/o scrolling is my repos..
+
 * drag and drop to reorder
-* cancel, pause, and resume **done**
-* keep it usable w/o javascript **done**
 * keep it accessible to blind, etc
 
 ## other features
@@ -30,7 +22,7 @@
 * Display something sane when kqueue runs out of file descriptors.
 * allow removing git remotes **done**
 * allow disabling syncing to here, which should temporarily disable all
-  local syncing.
+  local syncing. **done**
 
 ## first start **done**
 
diff --git a/doc/design/assistant/xmpp.mdwn b/doc/design/assistant/xmpp.mdwn
--- a/doc/design/assistant/xmpp.mdwn
+++ b/doc/design/assistant/xmpp.mdwn
@@ -9,12 +9,11 @@
 * Do git-annex clients sharing an account with regular clients cause confusing
   things to happen? 
   See <http://git-annex.branchable.com/design/assistant/blog/day_114__xmpp/#comment-aaba579f92cb452caf26ac53071a6788>
-* Assistant.Sync.manualPull doesn't handle XMPP remotes yet.
-  This is needed to handle getting back in sync after reconnection.
-* Support use of a single XMPP account with several separate git-annex repos.
-  This probably works for the simple push notification use of XMPP. But
-  XMPP pairing and the pushes over XMPP assume that anyone you're paired with
-  is intending to sync to your repository.
+* Support use of a single XMPP account with several separate and
+  independant git-annex repos. This probably works for the simple
+  push notification use of XMPP, since unknown UUIDs will just be ignored.
+  But XMPP pairing and the pushes over XMPP assume that anyone you're
+  paired with is intending to sync to your repository.
 
 ## design goals
 
diff --git a/doc/footer/column_a.mdwn b/doc/footer/column_a.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/footer/column_a.mdwn
@@ -0,0 +1,7 @@
+### Recent [[news]]
+
+[[!inline pages="news/* and !*/Discussion" archive=yes show=2 feeds=no]]
+
+### [[Dev blog|design/assistant/blog]]
+
+[[!inline pages="design/assistant/blog/* and !*/Discussion" archive=yes show=5 feeds=no]]
diff --git a/doc/footer/column_b.mdwn b/doc/footer/column_b.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/footer/column_b.mdwn
@@ -0,0 +1,7 @@
+### Recent [[videos]]
+
+[[!inline pages="videos/* and !*/Discussion" archive=yes show=2 feeds=no]]
+
+### Recent [[forum posts|forum]]
+
+[[!inline pages="forum/* and !*/Discussion" archive=yes show=5 feeds=no]]
diff --git a/doc/forum.mdwn b/doc/forum.mdwn
--- a/doc/forum.mdwn
+++ b/doc/forum.mdwn
@@ -1,5 +1,8 @@
 This is a place to discuss using git-annex.
 If you need help, advice, or anything, post about it here.
-(But [[post_bug_reports_over_here|bugs]].)
+
+But, please don't post bug reports here. Put them in [[bugs]].  
+And please don't make wishlist requests here. Put them in [[todo]].  
+(If you post bugs/todo here, it'll just get moved.)
 
 [[!inline pages="forum/* and !*/Discussion" archive=yes rootpage=forum postformtext="Add a new thread titled:"]]
diff --git a/doc/forum/Centralized_repository_with_webapp.mdwn b/doc/forum/Centralized_repository_with_webapp.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/forum/Centralized_repository_with_webapp.mdwn
@@ -0,0 +1,13 @@
+Hi,
+
+I'm kind of new to git-annex, I've been following it for a while and tried small task, but never used it in a real situation.
+I'm now trying to sync various computers through a central server and I'm having some problems, so I think I might be doing something wrong.
+
+I have a remote server that I want to use as central server. I use the webapp to configure client 1 to use that server as remote server and using git (so I assume it stores the files and the tree). I then create the client 2 in another computer and doing the exact same steps.
+Initially everything seams to work, but after a few modifications in the clients weird things start to happen, files only in one client, some files don't get updated, etc.
+
+I guess I'm doing something wrong (maybe I need to clone the repo in client 2 instead of creating a new one?) but I can't figure out how to solve it.
+
+Any tip that could help me?
+
+Thanks!
diff --git a/doc/forum/Difference_between_copy__44___move_and_get__63__.mdwn b/doc/forum/Difference_between_copy__44___move_and_get__63__.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/forum/Difference_between_copy__44___move_and_get__63__.mdwn
@@ -0,0 +1,24 @@
+I'm starting to experiment with git annex.  I'd like to use it for a centralized git repo that will be checked out often, but the clones will rarely need some large binary files (used for testing).  Therefore, I've set up a centralized/bare git repo and a clone of that repo using the instructions at [centralized_git_repository_tutorial](http://git-annex.branchable.com/tips/centralized_git_repository_tutorial/) and [bare_repositories](http://git-annex.branchable.com/bare_repositories/).  I've added some files to the annex in the clone.
+
+I'm struggling to understand the difference between copy, move, and get.  Here's a sequence of commands:
+
+    >> git annex add shared/1bel.maegz
+    >> git commit -m "added first file"
+    >> git push
+    >> git annex move shared/1bel.maegz --to origin
+    ## Now it no longer exists in my local repo
+    >> git annex get shared/1bel.maegz
+    fails.
+    >> git annex get shared/1bel.maegz --from origin
+    fails.
+    >> git annex copy shared/1bel.maegz --from origin
+    fails.
+    >> git annex move shared/1bel.maegz --from origin
+    succeeds! Now I have the file in my clone.
+
+Each failure message is:
+
+    fatal: Could not switch to '../.git/annex/objects/W8/gZ/SHA256-s99196--62874e9b58e652c9c01e796c2bf38b2234a80e0cef95c185bb7f0857d9765df2': No such file or directory
+    git-annex: <file descriptor: 6>: hGetLine: end of file
+
+How are copy, move, and get different? Which one *should* I be using to move my large data into the central (bare) repo?  Will it then be available to other clones?
diff --git a/doc/forum/Ubuntu_PPA.mdwn b/doc/forum/Ubuntu_PPA.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/forum/Ubuntu_PPA.mdwn
@@ -0,0 +1,3 @@
+Is there a PPA that Ubuntu people are using to track the latest git-annex?
+
+The [Ubuntu install page](http://git-annex.branchable.com/install/Ubuntu/) mentions [https://launchpad.net/~rubiojr/+archive/git-annex](https://launchpad.net/~rubiojr/+archive/git-annex) which hasn't been updated for a while.
diff --git a/doc/forum/Ubuntu_PPA/comment_1_b55535258b1b4bcfc802235f0cba075d._comment b/doc/forum/Ubuntu_PPA/comment_1_b55535258b1b4bcfc802235f0cba075d._comment
new file mode 100644
--- /dev/null
+++ b/doc/forum/Ubuntu_PPA/comment_1_b55535258b1b4bcfc802235f0cba075d._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawmeki7KlfJpAAN9CUmi9EdwhwBmIhDMYuE"
+ nickname="Marvin"
+ subject="comment 1"
+ date="2013-03-11T09:40:43Z"
+ content="""
+im also looking for a PPA with newer git-annex versions. mostly because of the glacier support...
+"""]]
diff --git a/doc/forum/Ubuntu_PPA/comment_2_adc4d644fed058d1811acf0b35db9c18._comment b/doc/forum/Ubuntu_PPA/comment_2_adc4d644fed058d1811acf0b35db9c18._comment
new file mode 100644
--- /dev/null
+++ b/doc/forum/Ubuntu_PPA/comment_2_adc4d644fed058d1811acf0b35db9c18._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawmVICFY2CDP08xdsPr3cgmScomy9HA-1sk"
+ nickname="Andrew"
+ subject="comment 2"
+ date="2013-03-13T02:31:52Z"
+ content="""
+It's perfectly possible (though slightly messy) to install git-annex on Ubuntu via [[/install/cabal]], though you need to find and install all the required dependencies yourself (haskell, libidn, libgnutls, libgsasl etc). If you want to get crazy with this look at <http://developer.ubuntu.com/packaging/html/>
+"""]]
diff --git a/doc/forum/git-remote-gcrypt.mdwn b/doc/forum/git-remote-gcrypt.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/forum/git-remote-gcrypt.mdwn
@@ -0,0 +1,1 @@
+Back in January Joey [mentioned](http://git-annex.branchable.com/design/assistant/blog/day_179__brief_updates/) the [git-remote-gcrypt](https://github.com/blake2-ppc/git-remote-gcrypt) and possibly adding support for it in the Assistant. I think this would be a great addition. Now that the first big Android push is complete, is there a schedule for this feature?
diff --git a/doc/forum/syncing_home_directories.mdwn b/doc/forum/syncing_home_directories.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/forum/syncing_home_directories.mdwn
@@ -0,0 +1,7 @@
+Hi,
+
+I synchronize home directories on a couple of machines using unison.  Is there a recommended way to do the equivalent using git-annex? 
+
+Thanks,
+
+John
diff --git a/doc/forum/wishlist:_command_options_changes.mdwn b/doc/forum/wishlist:_command_options_changes.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_command_options_changes.mdwn
+++ /dev/null
@@ -1,16 +0,0 @@
-Some suggestions for changes to command options:
-
-  * --verbose:
-    * add alternate: -v
-
-  * --from:
-    * replace with: -s $SOURCE || --source=$SOURCE
-
-  * --to:
-    * replace with: -d $DESTINATION || --destination=$DESTINATION
-
-  * --force:
-    * add alternate: -F
-      * "-f" was removed in v0.20110417
-      * since it forces unsafe operations, should be capitalized to reduce chance of accidental usage.
-
diff --git a/doc/forum/wishlist:_define_remotes_that_must_have_all_files.mdwn b/doc/forum/wishlist:_define_remotes_that_must_have_all_files.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_define_remotes_that_must_have_all_files.mdwn
+++ /dev/null
@@ -1,18 +0,0 @@
-I would like to be able to name a few remotes that must retain *all* annexed
-files.  `git-annex fsck` should warn me if any files are missing from those
-remotes, even if `annex.numcopies` has been satisfied by other remotes.
-
-I imagine this could also be useful for bup remotes, but I haven't actually
-looked at those yet.
-
-Based on existing output, this is what a warning message could look like:
-
-	fsck FILE
-		3 of 3 trustworthy copies of FILE exist.
-		FILE is, however, still missing from these required remotes:
-			UUID -- Backup Drive 1
-			UUID -- Backup Drive 2
-		Back it up with git-annex copy.
-	Warning
-
-What do you think?
diff --git a/doc/forum/wishlist:_do_round_robin_downloading_of_data.mdwn b/doc/forum/wishlist:_do_round_robin_downloading_of_data.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_do_round_robin_downloading_of_data.mdwn
+++ /dev/null
@@ -1,5 +0,0 @@
-Given that git/config will have information on remotes and maybe costs, it might be a good idea to do a simple round robin selection of remotes to download files where the costs are the same.
-
-This of course assumes that we like the idea of "parallel" launching and running of curl/rsync processes...
-
-This wish item is probably only useful for the paranoid people who store more than 1 copy of their data.
diff --git a/doc/forum/wishlist:_git-annex_replicate.mdwn b/doc/forum/wishlist:_git-annex_replicate.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_git-annex_replicate.mdwn
+++ /dev/null
@@ -1,12 +0,0 @@
-I'd like to be able to do something like the following:
-
- * Create encrypted git-annex remotes on a couple of semi-trusted machines - ones that have good connectivity, but non-redundant hardware
- * set numcopies=3
- * run `git-annex replicate` and have git-annex run the appropriate copy commands to make sure every file is on at least 3 machines
-
-There would also likely be a `git annex rebalance` command which could be used if remotes were added or removed.  If possible, it should copy files between servers directly, rather than proxy through a potentially slow client.
-
-There might be the need to have a 'replication_priority' option for each remote that configures which machines would be preferred.  That way you could set your local server to a high priority to ensure that it is always 1 of the 3 machines used and files are distributed across 2 of the remaining remotes.  Other than priority, other options that might help:
-
- * maxspace - A self imposed quota per remote machine.  git-annex replicate should try to replicate files first to machines with more free space. maxspace would change the free space calculation to be `min(actual_free_space, maxspace - space_used_by_git_annex)
- * bandwidth - when replication files, copies should be done between machines with the highest available bandwidth. ( I think this option could be useful for git-annex get in general)
diff --git a/doc/forum/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults.mdwn b/doc/forum/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults.mdwn
+++ /dev/null
@@ -1,17 +0,0 @@
-I am running centralized git-annex exclusively.
-
-Similar to
-
-    git annex get
-
-I'd like to have a
-
-    git annex put
-
-which would put all files on the default remote(s).
-
-My main reason for not wanting to use copy --to is that I need to specify the remote's name in this case which makes writing a wrapper unnecessarily hard. Also, this would allow
-
-    mr push
-
-to do the right thing all by itself.
diff --git a/doc/forum/wishlist:_git_annex_status.mdwn b/doc/forum/wishlist:_git_annex_status.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_git_annex_status.mdwn
+++ /dev/null
@@ -1,19 +0,0 @@
-Ideally, it would look similar to this. And yes, I put "put" in there ;)
-
-    non-annex % git annex status
-    git annex status: error: not a git annex repository
-    annex % git annex status
-    annex object storage version: A
-    annex backend engine: {WORM,SHA512,...}
-    Estimated local annex size: B MiB
-    Estimated total annex size: C MiB
-    Files without file size information in local annex: D
-    Files without file size information in total annex: E
-    Last fsck: datetime
-    Last git pull: datetime - $annex_name
-    Last git push: datetime - $annex_name
-    Last git annex get: datetime - $annex_name
-    Last git annex put: datetime - $annex_name
-    annex %
-
-Datetime could be ISO's YYYY-MM-DDThh:mm:ss or, personal preference, YYYY-MM-DD--hh-mm-ss. I prefer the latter as it's DNS-, tag- and filename-safe which is why I am using it for everything. In a perfect world, ISO would standardize YYYY-MM-DD-T-hh-mm-ss-Z[-SSSSSSSS][--$timezone], but meh.
diff --git a/doc/forum/wishlist:_git_backend_for_git-annex.mdwn b/doc/forum/wishlist:_git_backend_for_git-annex.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_git_backend_for_git-annex.mdwn
+++ /dev/null
@@ -1,7 +0,0 @@
-Preamble: Obviously, the core feature of git-annex is the ability to keep a subset of files in a local repo. The main trade-off is that you don't get version tracking.
-
-Use case: On my laptop, I might not have enough disk space to store everything. Not so for my main box nor my backup server. And I would _really_ like to have proper version tracking for many of my files. Thus...
-
-Wish: ...why not use git as a version backend? That way, I could just push all my stuff to the central instance(s) and have the best of both worlds. Depending on what backend is used in the local repos, it might make sense to define a list of supported client backends with pre-computed keys.
-
--- RichiH
diff --git a/doc/forum/wishlist:_simpler_gpg_usage.mdwn b/doc/forum/wishlist:_simpler_gpg_usage.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_simpler_gpg_usage.mdwn
+++ /dev/null
@@ -1,10 +0,0 @@
-This is my current understanding on how one must use gpg with git-annex:
-
- * Generate(or copy around) a gpg key on every machine that needs to access the encrypted remote.
- * git annex initremote myremote encryption=KEY for each key that you generated
-
-What I'm trying to figure out is if I can generate a no-passphrase gpg key and commit it to the repository, and have git-annex use that. That way any new clones of the annex automatically have access to any encrypted remotes, without having to do any key management.
-
-I think I can generate a no-passphrase key, but then I still have to manually copy it around to each machine.
-
-I'm not a huge gpg user so part of this is me wanting to avoid having to manage and keeping track of the keys.  This would probably be a non-issue if I used gpg on more machines and was more comfortable with it.
diff --git a/doc/forum/wishlist:_special_remote_for_sftp_or_rsync.mdwn b/doc/forum/wishlist:_special_remote_for_sftp_or_rsync.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_special_remote_for_sftp_or_rsync.mdwn
+++ /dev/null
@@ -1,26 +0,0 @@
-i think it would be useful to have a fourth kind of [[special_remotes]]
-that connects to a dumb storage using sftp or rsync. this can be emulated
-by using sshfs, but that means lots of round-trips through the system and
-is limited to platforms where sshfs is available.
-
-typical use cases are backups to storate shared between a group of people
-where each user only has limited access (sftp or rsync), when using
-[[special_remotes/bup]] is not an option.
-
-an alternative to implementing yet another special remote would be to have
-some kind of plugin system by which external programs can provide an
-interface to key-value stores (i'd implement the sftp backend myself, but
-haven't learned haskell yet).
-
-> Ask and ye [[shall receive|special_remotes/rsync]].
-> 
-> Sometimes I almost think that a generic configurable special remote that
-> just uses configured shell commands would be useful.. But there's really
-> no comparison with sitting down and writing code tuned to work with
-> a given transport like rsync, when it comes to reliability and taking
-> advantage of its abilities (like resuming). --[[Joey]]
-
->> big thanks, and bonus points for identical formats, so converting from
->> directory to rsync is just a matter of changing ``type`` from ``directory``
->> to ``rsync`` in ``.git-annex/remote.log`` and replacing the directory info
->> with ``annex-rsyncurl = <host>:<dir>`` in ``.git/config``. --[[chrysn]]
diff --git a/doc/forum/wishlist:_traffic_accounting_for_git-annex.mdwn b/doc/forum/wishlist:_traffic_accounting_for_git-annex.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:_traffic_accounting_for_git-annex.mdwn
+++ /dev/null
@@ -1,3 +0,0 @@
-As git annex keeps logs about file transfers anyway, it should be relatively easy to add traffic accounting to a repo. That would allow me to monitor how much traffic a given repo generates. As I might end up hosting git-annex repos for a few personal friends, I need/want a way to track the heavy hitters. -- RichiH
-
-PS: If you ever plan to host git-annex similar branchable, this would probably be of interest to you, as well :)
diff --git a/doc/forum/wishlist:alias_system.mdwn b/doc/forum/wishlist:alias_system.mdwn
deleted file mode 100644
--- a/doc/forum/wishlist:alias_system.mdwn
+++ /dev/null
@@ -1,1 +0,0 @@
-To implement things like my custom `git annex-push` without the dash, i.e. `git annex push`, an alias system for git-annex would be nice.
diff --git a/doc/git-annex.mdwn b/doc/git-annex.mdwn
--- a/doc/git-annex.mdwn
+++ b/doc/git-annex.mdwn
@@ -154,6 +154,9 @@
 
   To avoid immediately downloading the url, specify --fast.
 
+  To avoid storing the size of the url's content, and accept whatever
+  is there at a future point, specify --relaxed. (Implies --fast.)
+
   Normally the filename is based on the full url, so will look like
   "www.example.com_dir_subdir_bigfile". For a shorter filename, specify
   --pathdepth=N. For example, --pathdepth=1 will use "dir/subdir/bigfile",
@@ -394,13 +397,24 @@
   To generate output suitable for the gource visualisation program,
   specify --gource.
 
-* status
+* status [directory ...]
 
   Displays some statistics and other information, including how much data
   is in the annex and a list of all known repositories.
 
   To only show the data that can be gathered quickly, use --fast.
 
+  When a directory is specified, shows only an abbreviated status
+  display for that directory. In this mode, all of the file matching
+  options can be used to filter the files that will be included in
+  the status.
+
+  For example, suppose you want to run "git annex get .", but
+  would first like to see how much disk space that will use.
+  Then run:
+
+	git annex status . --not --in here
+
 * map
 
   Helps you keep track of your repositories, and the connections between them,
@@ -893,10 +907,19 @@
   For example, to limit the bandwidth to 100Kbyte/s, set it to "--bwlimit 100k"
   (There is no corresponding option for bup join.)
 
-* `annex.ssh-options`, `annex.rsync-options`, `annex.bup-split-options`
+* `remote.<name>.annex-gnupg-options`
 
-  Default ssh, rsync, wget/curl, and bup options to use if a remote does not
-  have specific options.
+  Options to pass to GnuPG for symmetric encryption. For instance, to
+  use the AES cipher with a 256 bits key and disable compression, set it
+  to "--cipher-algo AES256 --compress-algo none". (These options take
+  precedence over the default GnuPG configuration, which is otherwise
+  used.)
+
+* `annex.ssh-options`, `annex.rsync-options`, `annex.bup-split-options`,
+  `annex.gnupg-options`
+
+  Default ssh, rsync, wget/curl, bup, and GnuPG options to use if a
+  remote does not have specific options.
 
 * `annex.web-options`
 
diff --git a/doc/index.mdwn b/doc/index.mdwn
--- a/doc/index.mdwn
+++ b/doc/index.mdwn
@@ -1,26 +1,7 @@
 [[!inline raw=yes pages="summary"]]
 
-To get a feel for git-annex, see the [[walkthrough]] or read about [[how_it_works]].
-
 [[!sidebar content="""
-[[!img logo_small.png link=no]]
-
-* **[[install]]**
-* [[assistant]]
-* [[tips]]
-* [[bugs]]
-* [[todo]]
-* [[forum]]
-* [[comments]]
-* [[contact]]
-* [[testimonials]]
-* <a href="http://flattr.com/thing/84843/git-annex"><img src="https://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" /></a>
-
-[[News]]:
-
-<small>
-[[!inline pages="news/* and !*/discussion" archive=yes show=3 feeds=no]]
-</small>
+[[!inline feeds=no template=bare pages=sidebar]]
 
 [[Feeds]]:
 
@@ -31,6 +12,14 @@
 
 <table>
 <tr>
+<td width="33%" valign="top">[[!inline feeds=no template=bare pages=links/key_concepts]]</td>
+<td width="33%" valign="top">[[!inline feeds=no template=bare pages=links/the_details]]</td>
+<td width="33%" valign="top">[[!inline feeds=no template=bare pages=links/other_stuff]]</td>
+</tr>
+</table>
+
+<table>
+<tr>
 <td width="50%" valign="top">[[!inline feeds=no template=bare pages=use_case/bob]]</td>
 <td width="50%" valign="top">[[!inline feeds=no template=bare pages=use_case/alice]]</td>
 </tr>
@@ -41,42 +30,12 @@
 keeping all your small important files in git, to managing your large
 files with git.
 
-## documentation
-
-* [[git-annex man page|git-annex]]
-* [[key-value backends|backends]] for data storage
-* [[special_remotes]] (including [[special_remotes/S3]] and [[special_remotes/bup]])
-* [[sync]]
-* [[encryption]]
-* [[bare_repositories]]
-* [[direct_mode]]
-* [[internals]]
-* [[scalability]]
-* [[design]]
-* [[related_software]]
-* [[what git annex is not|not]]
-* [[sitemap]]
-
-## talks
-
 <table>
 <tr>
-<td width="50%" valign="top">
-<video controls
-src="http://video.fosdem.org/2012/lightningtalks/git_annex___manage_files_with_git,_without_checking_their_contents_into_git.webm" width="100%"></video><br>
-A <a href="http://video.fosdem.org/2012/lightningtalks/git_annex___manage_files_with_git,_without_checking_their_contents_into_git.webm">15 minute introduction to git-annex</a>, presented by Richard Hartmann at Fosdem 2012.
-</td>
-<td width="50%" valign="top">
-<video controls width="100%">
-<source type="video/mp4" src="http://mirror.linux.org.au/linux.conf.au/2013/mp4/gitannex.mp4">
-<source type="video/ogg" src="http://mirror.linux.org.au/linux.conf.au/2013/ogv/gitannex.ogv">
-</video><br>
-A <a href="http://mirror.linux.org.au/linux.conf.au/2013/mp4/gitannex.mp4">45 minute demo of git-annex and the assistant</a>), presented by Joey Hess at LCA 2013.
-</td>
+<td width="50%" valign="top">[[!inline feeds=no template=bare pages=footer/column_a]]</td>
+<td widtd="50%" valign="top">[[!inline feeds=no template=bare pages=footer/column_b]]</td>
 </tr>
 </table>
-
-<br clear="all" />
 
 ----
 
diff --git a/doc/install/Android.mdwn b/doc/install/Android.mdwn
--- a/doc/install/Android.mdwn
+++ b/doc/install/Android.mdwn
@@ -19,19 +19,20 @@
 
 A daily build is also available.
 
-* [download tarball](http://downloads.kitenet.net/git-annex/autobuild/i386/git-annex-standalone-i386.tar.gz) ([build logs](http://downloads.kitenet.net/git-annex/autobuild/android/))
+* [download apk](http://downloads.kitenet.net/git-annex/autobuild/android/git-annex.apk) ([build logs](http://downloads.kitenet.net/git-annex/autobuild/android/))
 
 ## building it yourself
 
-git-annex can be built for Android, with `make android`.
-You need <https://github.com/neurocyte/ghc-android> installed first,
-and also have to `cabal install` all necessary dependencies. This is not
-yet an easy process. 
-
-You also need to install git and all the utilities listed on [[fromscratch]].
-
-You will need to have the Android SDK and NDK installed; see
-`standalone/android/Makefile` to configure the paths to them. You'll also
-need ant, and the JDK.
+git-annex can be built for Android, with `make android`. It's not an easy
+process:
 
-Then to build the full Android app bundle, use `make androidapp`
+* First, install <https://github.com/neurocyte/ghc-android>.
+* You also need to install git and all the utilities listed on [[fromscratch]],
+  on the system doing the building.
+* Use ghc-android's cabal to install all necessary dependencies.
+  Some packages will fail to install on Android; patches to fix them
+  are in `standalone/android/haskell-patches/`
+* You will need to have the Android SDK and NDK installed; see
+  `standalone/android/Makefile` to configure the paths to them. You'll also
+  need ant, and the JDK.
+* Then to build the full Android app bundle, use `make androidapp`
diff --git a/doc/install/fromscratch.mdwn b/doc/install/fromscratch.mdwn
--- a/doc/install/fromscratch.mdwn
+++ b/doc/install/fromscratch.mdwn
@@ -10,7 +10,6 @@
   * [dataenc](http://hackage.haskell.org/package/dataenc)
   * [monad-control](http://hackage.haskell.org/package/monad-control)
   * [lifted-base](http://hackage.haskell.org/package/lifted-base)
-  * [TestPack](http://hackage.haskell.org/cgi-bin/hackage-scripts/package/testpack)
   * [QuickCheck 2](http://hackage.haskell.org/package/QuickCheck)
   * [json](http://hackage.haskell.org/package/json)
   * [IfElse](http://hackage.haskell.org/package/IfElse)
@@ -20,7 +19,7 @@
   * [DAV](http://hackage.haskell.org/package/DAV) (optional)
   * [SafeSemaphore](http://hackage.haskell.org/package/SafeSemaphore)
   * [UUID](http://hackage.haskell.org/package/uuid)
-  * [Glob](http://hackage.haskell.org/package/Glob)
+  * [regex-tdfa](http://hackage.haskell.org/package/regex-tdfa)
 * Optional haskell stuff, used by the [[assistant]] and its webapp (edit Makefile to disable)
   * [stm](http://hackage.haskell.org/package/stm)
     (version 2.3 or newer)
diff --git a/doc/internals.mdwn b/doc/internals.mdwn
--- a/doc/internals.mdwn
+++ b/doc/internals.mdwn
@@ -51,14 +51,22 @@
 If there are multiple lines for the same uuid, the one with the most recent
 timestamp wins. git-annex union merges this and other files.
 
-## `remotes.log`
+## `remote.log`
 
 Holds persistent configuration settings for [[special_remotes]] such as
 Amazon S3.
 
 The file format is one line per remote, starting with the uuid of the
-remote, followed by a space, and then a series of key=value pairs,
+remote, followed by a space, and then a series of var=value pairs,
 each separated by whitespace, and finally a timestamp.
+
+Encrypted special remotes store their encryption key here,
+in the "cipher" value. It is base64 encoded, and unless shared [[encryption]]
+is used, is encrypted to one or more gpg keys. The first 256 bytes of
+the cipher is used as the HMAC SHA1 encryption key, to encrypt filenames
+stored on the special remote. The remainder of the cipher is used as a gpg
+symmetric encryption key, to encrypt the content of files stored on the special
+remote.
 
 ## `trust.log`
 
diff --git a/doc/links/key_concepts.mdwn b/doc/links/key_concepts.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/links/key_concepts.mdwn
@@ -0,0 +1,7 @@
+### key concepts
+
+* [[git-annex man page|git-annex]]
+* [[how_it_works]]
+* [[special_remotes]]
+* [[sync]]
+* [[direct_mode]]
diff --git a/doc/links/other_stuff.mdwn b/doc/links/other_stuff.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/links/other_stuff.mdwn
@@ -0,0 +1,6 @@
+### other stuff
+
+* [[testimonials]]
+* [[what git annex is not|not]]
+* [[related_software]]
+* [[sitemap]]
diff --git a/doc/links/the_details.mdwn b/doc/links/the_details.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/links/the_details.mdwn
@@ -0,0 +1,8 @@
+### the details
+
+* [[encryption]]
+* [[key-value backends|backends]]
+* [[bare_repositories]]
+* [[internals]]
+* [[scalability]]
+* [[design]]
diff --git a/doc/news/version_3.20130114.mdwn b/doc/news/version_3.20130114.mdwn
deleted file mode 100644
--- a/doc/news/version_3.20130114.mdwn
+++ /dev/null
@@ -1,23 +0,0 @@
-git-annex 3.20130114 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
-   * Now handles the case where a file that's being transferred to a remote
-     is modified in place, which direct mode allows. When this
-     happens, the transfer now fails, rather than allow possibly corrupt
-     data into the remote.
-   * fsck: Better checking of file content in direct mode.
-   * drop: Suggest using git annex move when numcopies prevents dropping a file.
-   * webapp: Repo switcher filters out repos that do not exist any more
-     (or are on a drive that's not mounted).
-   * webapp: Use IP address, rather than localhost, since some systems may
-     have configuration problems or other issues that prevent web browsers
-     from connecting to the right localhost IP for the webapp.
-   * webapp: Adjust longpoll code to work with recent versions of
-     shakespeare-js.
-   * assistant: Support new gvfs dbus names used in Gnome 3.6.
-   * In direct mode, files with the same key are no longer hardlinked, as
-     that would cause a surprising behavior if modifying one, where the other
-     would also change.
-   * webapp: Avoid illegal characters in hostname when creating S3 or
-     Glacier remote.
-   * assistant: Avoid committer crashing if a file is deleted at the wrong
-     instant."""]]
diff --git a/doc/news/version_4.20130314.mdwn b/doc/news/version_4.20130314.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/news/version_4.20130314.mdwn
@@ -0,0 +1,68 @@
+git-annex 4.20130314 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+   * Bugfix: git annex add, when ran without any file or directory specified,
+     should add files in the current directory, but not act on unlocked files
+     elsewhere in the tree.
+   * Bugfix: drop --from an unavailable remote no longer updates the location
+     log, incorrectly, to say the remote does not have the key.
+   * Bugfix: If the UUID of a remote is not known, prevent --from, --to,
+     and other ways of specifying remotes by name from selecting it,
+     since it is not possible to sanely use it.
+   * Bugfix: Fix bug in inode cache sentinal check, which broke
+     copying to local repos if the repo being copied from had moved
+     to a different filesystem or otherwise changed all its inodes
+   * Switch from using regex-compat to regex-tdfa, as the C regex library
+     is rather buggy.
+   * status: Can now be run with a directory path to show only the
+     status of that directory, rather than the whole annex.
+   * Added remote.&lt;name&gt;.annex-gnupg-options setting.
+     Thanks, guilhem for the patch.
+   * addurl: Add --relaxed option.
+   * addurl: Escape invalid characters in urls, rather than failing to
+     use an invalid url.
+   * addurl: Properly handle url-escaped characters in file:// urls.
+   * assistant: Fix dropping content when a file is moved to an archive
+     directory, and getting contennt when a file is moved back out.
+   * assistant: Fix bug in direct mode that could occur when a symlink is
+     moved out of an archive directory, and resulted in the file not being
+     set to direct mode when it was transferred.
+   * assistant: Generate better commits for renames.
+   * assistant: Logs are rotated to avoid them using too much disk space.
+   * assistant: Avoid noise in logs from git commit about typechanged
+     files in direct mode repositories.
+   * assistant: Set gc.auto=0 when creating repositories to prevent
+     automatic commits from causing git-gc runs.
+   * assistant: If gc.auto=0, run git-gc once a day, packing loose objects
+     very non-aggressively.
+   * assistant: XMPP git pull and push requests are cached and sent when
+     presence of a new client is detected.
+   * assistant: Sync with all git remotes on startup.
+   * assistant: Get back in sync with XMPP remotes after network reconnection,
+     and on startup.
+   * assistant: Fix syncing after XMPP pairing.
+   * assistant: Optimised handling of renamed files in direct mode,
+     avoiding re-checksumming.
+   * assistant: Detects most renames, including directory renames, and
+     combines all their changes into a single commit.
+   * assistant: Fix ~/.ssh/git-annex-shell wrapper to work when the
+     ssh key does not force a command.
+   * assistant: Be smarter about avoiding unncessary transfers.
+   * webapp: Work around bug in Warp's slowloris attack prevention code,
+     that caused regular browsers to stall when they reuse a connection
+     after leaving it idle for 30 seconds.
+     (See https://github.com/yesodweb/wai/issues/146)
+   * webapp: New preferences page allows enabling/disabling debug logging
+     at runtime, as well as configuring numcopies and diskreserve.
+   * webapp: Repository costs can be configured by dragging repositories around
+     in the repository list.
+   * webapp: Proceed automatically on from "Configure jabber account"
+     to pairing.
+   * webapp: Only show up to 10 queued transfers.
+   * webapp: DTRT when told to create a git repo that already exists.
+   * webapp: Set locally paired repositories to a lower cost than other
+     network remotes.
+   * Run ssh with -T to avoid tty allocation and any login scripts that
+     may do undesired things with it.
+   * Several improvements to Makefile and cabal file. Thanks, Peter Simmons
+   * Stop depending on testpack.
+   * Android: Enable test suite."""]]
diff --git a/doc/sidebar.mdwn b/doc/sidebar.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/sidebar.mdwn
@@ -0,0 +1,12 @@
+[[!img logo_small.png link=no]]
+
+* [[install]]
+* [[assistant]]
+* [[walkthrough]]
+* [[tips]]
+* [[bugs]]
+* [[todo]]
+* [[forum]]
+* [[comments]]
+* [[contact]]
+* <a href="http://flattr.com/thing/84843/git-annex"><img src="https://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" /></a>
diff --git a/doc/sitemap.mdwn b/doc/sitemap.mdwn
--- a/doc/sitemap.mdwn
+++ b/doc/sitemap.mdwn
@@ -1,3 +1,4 @@
 [[!map pages="page(*) and !*/discussion and !recentchanges
-and !bugs/* and !examples/*/* and !news/* and !tips/*
+and !bugs/* and !examples/*/* and !news/* and !tips/* and !videos/*
+and !design/assistant/blog/*
 and !forum/* and !todo/* and !users/* and !ikiwiki/*"]]
diff --git a/doc/special_remotes/bup/comment_3_1186def82741ddab1ade256fb2e59e6f._comment b/doc/special_remotes/bup/comment_3_1186def82741ddab1ade256fb2e59e6f._comment
new file mode 100644
--- /dev/null
+++ b/doc/special_remotes/bup/comment_3_1186def82741ddab1ade256fb2e59e6f._comment
@@ -0,0 +1,17 @@
+[[!comment format=mdwn
+ username="http://sekenre.wordpress.com/"
+ nickname="sekenre"
+ subject="Bup remotes in git-annex assistant"
+ date="2013-03-13T12:54:56Z"
+ content="""
+Hi,
+
+Is the bup remote available via the Assistant user interface?
+
+Unrelated question;
+
+If you are syncing files between two bup repos on local usb drives, does it use git to sync the changes or does it use \"bup split\" to re-add the file? (Basically, is the syncing as efficient as possible using git-annex or would I have to go to a lower level)
+
+Many Thanks,
+Sek
+"""]]
diff --git a/doc/special_remotes/bup/comment_4_7d22a805dd2914971e7ca628ceea69be._comment b/doc/special_remotes/bup/comment_4_7d22a805dd2914971e7ca628ceea69be._comment
new file mode 100644
--- /dev/null
+++ b/doc/special_remotes/bup/comment_4_7d22a805dd2914971e7ca628ceea69be._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ nickname="joey"
+ subject="comment 4"
+ date="2013-03-13T16:05:50Z"
+ content="""
+I don't plan to support creating bup spefial remotes in the assistant, currently. Of course the assistant can use bup special remotes you set up.
+
+Your two bup repos would be synced using bup-split.
+"""]]
diff --git a/doc/special_remotes/bup/comment_5_61b32f9ee00e6016443a1cf10273959c._comment b/doc/special_remotes/bup/comment_5_61b32f9ee00e6016443a1cf10273959c._comment
new file mode 100644
--- /dev/null
+++ b/doc/special_remotes/bup/comment_5_61b32f9ee00e6016443a1cf10273959c._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="http://joeyh.name/"
+ nickname="joey"
+ subject="comment 5"
+ date="2013-03-13T16:05:57Z"
+ content="""
+I don't plan to support creating bup spefial remotes in the assistant, currently. Of course the assistant can use bup special remotes you set up.
+
+Your two bup repos would be synced using bup-split.
+"""]]
diff --git a/doc/summary.mdwn b/doc/summary.mdwn
--- a/doc/summary.mdwn
+++ b/doc/summary.mdwn
@@ -7,3 +7,5 @@
 git-annex is designed for git users who love the command line.
 For everyone else, the [[git-annex assistant|assistant]] turns
 git-annex into an easy to use folder synchroniser.
+
+To get a feel for git-annex, see the [[walkthrough]].
diff --git a/doc/todo/union_mounting/comment_1_cb08435812dd7766de26199c73f38e8b._comment b/doc/todo/union_mounting/comment_1_cb08435812dd7766de26199c73f38e8b._comment
new file mode 100644
--- /dev/null
+++ b/doc/todo/union_mounting/comment_1_cb08435812dd7766de26199c73f38e8b._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="https://www.google.com/accounts/o8/id?id=AItOawln3ckqKx0x_xDZMYwa9Q1bn4I06oWjkog"
+ nickname="Michael"
+ subject="comment 1"
+ date="2013-03-01T01:26:36Z"
+ content="""
+This would indeed be very helpful when remotely mounting a photo/video collection over samba.
+"""]]
diff --git a/doc/todo/union_mounting/comment_2_240b1736f6bd4fbf87c372d3a46e661b._comment b/doc/todo/union_mounting/comment_2_240b1736f6bd4fbf87c372d3a46e661b._comment
new file mode 100644
--- /dev/null
+++ b/doc/todo/union_mounting/comment_2_240b1736f6bd4fbf87c372d3a46e661b._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="http://edheil.wordpress.com/"
+ ip="173.162.44.162"
+ subject="comment 2"
+ date="2013-03-01T04:50:28Z"
+ content="""
++1 this would be sweet as hell
+
+"""]]
diff --git a/doc/todo/wishlist:_GnuPG_options.mdwn b/doc/todo/wishlist:_GnuPG_options.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_GnuPG_options.mdwn
@@ -0,0 +1,16 @@
+[Maybe I should have extented [[wishlist:_simpler_gpg_usage/]], but I thought I'd make my own since it's perhaps too old.]
+
+I second Justin and [[his idea|wishlist:_simpler_gpg_usage/#comment-e120f8ede0d4cffce17cbf84564211c1]] of having per-remote GnuPG options. I'd even go one step further, and propose the option in the <tt>.gitattributes</tt> file. Indeed by default GnuPG compresses the data before encryption, which doesn't make a lot of sense for git-annex (in my use-case at least); My work-around to save this waste of CPU cycles was to customize my <tt>gpg.conf</tt>, but it's somewhat dirty since I do want to use compression in general.
+
+Here is how I envision the <tt>.git/config</tt>:
+<pre>    <code>[annex]
+        gnupg-options = --s2k-cipher-algo AES256 --s2k-digest-algo SHA512 --s2k-count 8388608 --cipher-algo AES256 --compress-algo none
+</code></pre>
+
+And compression could be enabled on say, text files, with a suitable wildcard in the <tt>.gitattributes</tt> file.
+<pre>    <code>*.txt annex.gnupg-options="--s2k-cipher-algo AES256 --s2k-digest-algo SHA512 --s2k-count 8388608 --cipher-algo AES256 --compress-algo zlib"
+</code></pre>
+
+This is something I could probably hack on if you think it'd be a worthwhile option ;-)
+
+> Done, and [[done]]! --[[Joey]]
diff --git a/doc/todo/wishlist:_command_options_changes.mdwn b/doc/todo/wishlist:_command_options_changes.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_command_options_changes.mdwn
@@ -0,0 +1,17 @@
+Some suggestions for changes to command options:
+
+  * --verbose:
+    * add alternate: -v
+
+  * --from:
+    * replace with: -s $SOURCE || --source=$SOURCE
+
+  * --to:
+    * replace with: -d $DESTINATION || --destination=$DESTINATION
+
+  * --force:
+    * add alternate: -F
+      * "-f" was removed in v0.20110417
+      * since it forces unsafe operations, should be capitalized to reduce chance of accidental usage.
+
+[[done]], see comments
diff --git a/doc/todo/wishlist:_define_remotes_that_must_have_all_files.mdwn b/doc/todo/wishlist:_define_remotes_that_must_have_all_files.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_define_remotes_that_must_have_all_files.mdwn
@@ -0,0 +1,18 @@
+I would like to be able to name a few remotes that must retain *all* annexed
+files.  `git-annex fsck` should warn me if any files are missing from those
+remotes, even if `annex.numcopies` has been satisfied by other remotes.
+
+I imagine this could also be useful for bup remotes, but I haven't actually
+looked at those yet.
+
+Based on existing output, this is what a warning message could look like:
+
+	fsck FILE
+		3 of 3 trustworthy copies of FILE exist.
+		FILE is, however, still missing from these required remotes:
+			UUID -- Backup Drive 1
+			UUID -- Backup Drive 2
+		Back it up with git-annex copy.
+	Warning
+
+What do you think?
diff --git a/doc/todo/wishlist:_do_round_robin_downloading_of_data.mdwn b/doc/todo/wishlist:_do_round_robin_downloading_of_data.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_do_round_robin_downloading_of_data.mdwn
@@ -0,0 +1,5 @@
+Given that git/config will have information on remotes and maybe costs, it might be a good idea to do a simple round robin selection of remotes to download files where the costs are the same.
+
+This of course assumes that we like the idea of "parallel" launching and running of curl/rsync processes...
+
+This wish item is probably only useful for the paranoid people who store more than 1 copy of their data.
diff --git a/doc/todo/wishlist:_git-annex_replicate.mdwn b/doc/todo/wishlist:_git-annex_replicate.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_git-annex_replicate.mdwn
@@ -0,0 +1,12 @@
+I'd like to be able to do something like the following:
+
+ * Create encrypted git-annex remotes on a couple of semi-trusted machines - ones that have good connectivity, but non-redundant hardware
+ * set numcopies=3
+ * run `git-annex replicate` and have git-annex run the appropriate copy commands to make sure every file is on at least 3 machines
+
+There would also likely be a `git annex rebalance` command which could be used if remotes were added or removed.  If possible, it should copy files between servers directly, rather than proxy through a potentially slow client.
+
+There might be the need to have a 'replication_priority' option for each remote that configures which machines would be preferred.  That way you could set your local server to a high priority to ensure that it is always 1 of the 3 machines used and files are distributed across 2 of the remaining remotes.  Other than priority, other options that might help:
+
+ * maxspace - A self imposed quota per remote machine.  git-annex replicate should try to replicate files first to machines with more free space. maxspace would change the free space calculation to be `min(actual_free_space, maxspace - space_used_by_git_annex)
+ * bandwidth - when replication files, copies should be done between machines with the highest available bandwidth. ( I think this option could be useful for git-annex get in general)
diff --git a/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults.mdwn b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_git_annex_put_--_same_as_get__44___but_for_defaults.mdwn
@@ -0,0 +1,17 @@
+I am running centralized git-annex exclusively.
+
+Similar to
+
+    git annex get
+
+I'd like to have a
+
+    git annex put
+
+which would put all files on the default remote(s).
+
+My main reason for not wanting to use copy --to is that I need to specify the remote's name in this case which makes writing a wrapper unnecessarily hard. Also, this would allow
+
+    mr push
+
+to do the right thing all by itself.
diff --git a/doc/todo/wishlist:_git_annex_status.mdwn b/doc/todo/wishlist:_git_annex_status.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_git_annex_status.mdwn
@@ -0,0 +1,21 @@
+Ideally, it would look similar to this. And yes, I put "put" in there ;)
+
+    non-annex % git annex status
+    git annex status: error: not a git annex repository
+    annex % git annex status
+    annex object storage version: A
+    annex backend engine: {WORM,SHA512,...}
+    Estimated local annex size: B MiB
+    Estimated total annex size: C MiB
+    Files without file size information in local annex: D
+    Files without file size information in total annex: E
+    Last fsck: datetime
+    Last git pull: datetime - $annex_name
+    Last git push: datetime - $annex_name
+    Last git annex get: datetime - $annex_name
+    Last git annex put: datetime - $annex_name
+    annex %
+
+Datetime could be ISO's YYYY-MM-DDThh:mm:ss or, personal preference, YYYY-MM-DD--hh-mm-ss. I prefer the latter as it's DNS-, tag- and filename-safe which is why I am using it for everything. In a perfect world, ISO would standardize YYYY-MM-DD-T-hh-mm-ss-Z[-SSSSSSSS][--$timezone], but meh.
+
+[[done]]
diff --git a/doc/todo/wishlist:_git_backend_for_git-annex.mdwn b/doc/todo/wishlist:_git_backend_for_git-annex.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_git_backend_for_git-annex.mdwn
@@ -0,0 +1,9 @@
+Preamble: Obviously, the core feature of git-annex is the ability to keep a subset of files in a local repo. The main trade-off is that you don't get version tracking.
+
+Use case: On my laptop, I might not have enough disk space to store everything. Not so for my main box nor my backup server. And I would _really_ like to have proper version tracking for many of my files. Thus...
+
+Wish: ...why not use git as a version backend? That way, I could just push all my stuff to the central instance(s) and have the best of both worlds. Depending on what backend is used in the local repos, it might make sense to define a list of supported client backends with pre-computed keys.
+
+-- RichiH
+
+[[done]] (bup)
diff --git a/doc/todo/wishlist:_option_to_disable_url_checking_with_addurl.mdwn b/doc/todo/wishlist:_option_to_disable_url_checking_with_addurl.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_option_to_disable_url_checking_with_addurl.mdwn
@@ -0,0 +1,9 @@
+I'm testing out an idea of using filter-branch on a git repository to both retroactively annex and AND record a weburl for all relevant files.
+
+c.f. [http://git-annex.branchable.com/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/](http://git-annex.branchable.com/tips/How_to_retroactively_annex_a_file_already_in_a_git_repo/)
+
+The bottleneck I'm hitting here seems to be the fact that `git annex addurl` diligently checks each url to see that it is accessible, which adds up quickly if many files are to be processed.
+
+It would be great if addurl had an option to disable checking the url, in order to speed up large batch jobs like this.
+
+> --relaxed added [[done]] --[[Joey]]
diff --git a/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl.mdwn b/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_recursive_directory_remote_setup__47__addurl.mdwn
@@ -0,0 +1,7 @@
+I think it would be interesting to have a way to recursively import a local directory without actually moving files around. And to be able to checksum these files as well (without moving them into the annex).
+
+This would work somewhat similar to looping over a directory and adding file:// remotes for each file.
+
+A use case is importing optical media (read-only), whilst keeping that media as a remote, and being able to calculate checksums directly without moving any files around.
+
+For single files, it would also be interesting if addurl had a "--localchecksum" option that would only work for file:// urls, and make it checksum files directly from their source location?)
diff --git a/doc/todo/wishlist:_simpler_gpg_usage.mdwn b/doc/todo/wishlist:_simpler_gpg_usage.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_simpler_gpg_usage.mdwn
@@ -0,0 +1,12 @@
+This is my current understanding on how one must use gpg with git-annex:
+
+ * Generate(or copy around) a gpg key on every machine that needs to access the encrypted remote.
+ * git annex initremote myremote encryption=KEY for each key that you generated
+
+What I'm trying to figure out is if I can generate a no-passphrase gpg key and commit it to the repository, and have git-annex use that. That way any new clones of the annex automatically have access to any encrypted remotes, without having to do any key management.
+
+I think I can generate a no-passphrase key, but then I still have to manually copy it around to each machine.
+
+I'm not a huge gpg user so part of this is me wanting to avoid having to manage and keeping track of the keys.  This would probably be a non-issue if I used gpg on more machines and was more comfortable with it.
+
+[[done]]
diff --git a/doc/todo/wishlist:_special_remote_for_sftp_or_rsync.mdwn b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_special_remote_for_sftp_or_rsync.mdwn
@@ -0,0 +1,28 @@
+i think it would be useful to have a fourth kind of [[special_remotes]]
+that connects to a dumb storage using sftp or rsync. this can be emulated
+by using sshfs, but that means lots of round-trips through the system and
+is limited to platforms where sshfs is available.
+
+typical use cases are backups to storate shared between a group of people
+where each user only has limited access (sftp or rsync), when using
+[[special_remotes/bup]] is not an option.
+
+an alternative to implementing yet another special remote would be to have
+some kind of plugin system by which external programs can provide an
+interface to key-value stores (i'd implement the sftp backend myself, but
+haven't learned haskell yet).
+
+> Ask and ye [[shall receive|special_remotes/rsync]].
+> 
+> Sometimes I almost think that a generic configurable special remote that
+> just uses configured shell commands would be useful.. But there's really
+> no comparison with sitting down and writing code tuned to work with
+> a given transport like rsync, when it comes to reliability and taking
+> advantage of its abilities (like resuming). --[[Joey]]
+
+>> big thanks, and bonus points for identical formats, so converting from
+>> directory to rsync is just a matter of changing ``type`` from ``directory``
+>> to ``rsync`` in ``.git-annex/remote.log`` and replacing the directory info
+>> with ``annex-rsyncurl = <host>:<dir>`` in ``.git/config``. --[[chrysn]]
+
+[[done]]
diff --git a/doc/todo/wishlist:_traffic_accounting_for_git-annex.mdwn b/doc/todo/wishlist:_traffic_accounting_for_git-annex.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:_traffic_accounting_for_git-annex.mdwn
@@ -0,0 +1,3 @@
+As git annex keeps logs about file transfers anyway, it should be relatively easy to add traffic accounting to a repo. That would allow me to monitor how much traffic a given repo generates. As I might end up hosting git-annex repos for a few personal friends, I need/want a way to track the heavy hitters. -- RichiH
+
+PS: If you ever plan to host git-annex similar branchable, this would probably be of interest to you, as well :)
diff --git a/doc/todo/wishlist:alias_system.mdwn b/doc/todo/wishlist:alias_system.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/todo/wishlist:alias_system.mdwn
@@ -0,0 +1,1 @@
+To implement things like my custom `git annex-push` without the dash, i.e. `git annex push`, an alias system for git-annex would be nice.
diff --git a/doc/videos.mdwn b/doc/videos.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos.mdwn
@@ -0,0 +1,8 @@
+Talks and screencasts about git-annex.
+
+These videos are also available in a public git-annex repository
+`git clone http://downloads.kitenet.net/.git/`
+
+[[!inline pages="./videos/* and !./videos/*/* and !*/Discussion" show="2"]]
+
+[[!inline pages="./videos/* and !./videos/*/* and !*/Discussion" show="0" archive=yes skip=2 feeds=no]]
diff --git a/doc/videos/FOSDEM2012.mdwn b/doc/videos/FOSDEM2012.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/FOSDEM2012.mdwn
@@ -0,0 +1,7 @@
+<video controls
+src="http://video.fosdem.org/2012/lightningtalks/git_annex___manage_files_with_git,_without_checking_their_contents_into_git.webm"></video><br>
+A <a href="http://video.fosdem.org/2012/lightningtalks/git_annex___manage_files_with_git,_without_checking_their_contents_into_git.webm">15 minute introduction to git-annex</a>,
+presented by Richard Hartmann at FOSDEM 2012.
+
+[[!meta date="1 Jan 2012"]]
+[[!meta title="git-annex presentation by Richard Hartmann at FOSDEM 2012"]]
diff --git a/doc/videos/LCA2013.mdwn b/doc/videos/LCA2013.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/LCA2013.mdwn
@@ -0,0 +1,8 @@
+<video controls>
+<source type="video/mp4" src="http://mirror.linux.org.au/linux.conf.au/2013/mp4/gitannex.mp4">
+<source type="video/ogg" src="http://mirror.linux.org.au/linux.conf.au/2013/ogv/gitannex.ogv">
+</video><br>
+A <a href="http://mirror.linux.org.au/linux.conf.au/2013/mp4/gitannex.mp4">45 minute talk and demo of git-annex and the assistant</a>), presented by Joey Hess at LCA 2013.
+
+[[!meta date="1 Feb 2013"]]
+[[!meta title="git-annex presentation by Joey Hess at Linux.Conf.Au 2013"]]
diff --git a/doc/videos/git-annex_assistant_archiving.mdwn b/doc/videos/git-annex_assistant_archiving.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/git-annex_assistant_archiving.mdwn
@@ -0,0 +1,5 @@
+<video controls width=400>
+<source type="video/mp4" src="http://downloads.kitenet.net/videos/git-annex/git-annex-assistant-archiving.ogv">
+</video><br>
+A <a href="http://downloads.kitenet.net/videos/git-annex/git-annex-assistant-archiving.ogv">9 minute screencast</a>
+covering archiving your files with the [[git-annex assistant|/assistant]]</a>.
diff --git a/doc/videos/git-annex_assistant_introduction.mdwn b/doc/videos/git-annex_assistant_introduction.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/git-annex_assistant_introduction.mdwn
@@ -0,0 +1,5 @@
+<video controls width=400>
+<source type="video/mp4" src="http://downloads.kitenet.net/videos/git-annex/git-annex-assistant-intro.ogv">
+</video><br>
+A <a href="http://downloads.kitenet.net/videos/git-annex/git-annex-assistant-intro.ogv">8 minute screencast</a>
+introducing the [[git-annex assistant|/assistant]]</a>.
diff --git a/doc/videos/git-annex_assistant_sync_demo.mdwn b/doc/videos/git-annex_assistant_sync_demo.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/git-annex_assistant_sync_demo.mdwn
@@ -0,0 +1,8 @@
+A screencast demoing the git-annex assistant syncing between Nicaragua
+and the United Kingdom for the first time.
+
+<video controls src="http://joeyh.name/screencasts/git-annex-assistant.ogg"></video>
+
+[video](http://joeyh.name/screencasts/git-annex-assistant.ogg)
+
+[[!meta date="Thu Jul 5 16:36:06 2012 -0600"]]
diff --git a/doc/videos/git-annex_watch_demo.mdwn b/doc/videos/git-annex_watch_demo.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/git-annex_watch_demo.mdwn
@@ -0,0 +1,7 @@
+A quick screencast demoing the `git annex watch` daemon.
+
+<video controls src="http://joeyh.name/screencasts/git-annex-watch.ogg"></video>
+
+[video](http://joeyh.name/screencasts/git-annex-watch.ogg)
+
+[[!meta date="Mon Jun 11 16:02:14 2012 -0400"]]
diff --git a/doc/videos/git-annex_weppapp_demo.mdwn b/doc/videos/git-annex_weppapp_demo.mdwn
new file mode 100644
--- /dev/null
+++ b/doc/videos/git-annex_weppapp_demo.mdwn
@@ -0,0 +1,8 @@
+A quick screencast demoing the early `git annex webapp` and
+automatic USB drive mount detection and syncing.
+
+<video controls src="http://joeyh.name/screencasts/git-annex-webapp.ogg"></video>
+
+[video](http://joeyh.name/screencasts/git-annex-webapp.ogg)
+
+[[!meta date="Sun Jul 29 14:41:41 2012 -0400"]]
diff --git a/ghci b/ghci
new file mode 100644
--- /dev/null
+++ b/ghci
@@ -0,0 +1,4 @@
+#!/bin/sh
+# ghci using objects built by cabal
+make dist/caballog
+$(grep 'ghc --make' dist/caballog | head -n 1 | perl -pe 's/--make/--interactive/; s/.\/[^\.\s]+.hs//; s/-package-id [^\s]+//g; s/-hide-all-packages//; s/-threaded//; s/-O//') $@
diff --git a/git-annex.1 b/git-annex.1
--- a/git-annex.1
+++ b/git-annex.1
@@ -140,6 +140,9 @@
 .IP
 To avoid immediately downloading the url, specify \-\-fast.
 .IP
+To avoid storing the size of the url's content, and accept whatever
+is there at a future point, specify \-\-relaxed. (Implies \-\-fast.)
+.IP
 Normally the filename is based on the full url, so will look like
 "www.example.com_dir_subdir_bigfile". For a shorter filename, specify
 \-\-pathdepth=N. For example, \-\-pathdepth=1 will use "dir/subdir/bigfile",
@@ -353,12 +356,23 @@
 To generate output suitable for the gource visualisation program,
 specify \-\-gource.
 .IP
-.IP "status"
+.IP "status [directory ...]"
 Displays some statistics and other information, including how much data
 is in the annex and a list of all known repositories.
 .IP
 To only show the data that can be gathered quickly, use \-\-fast.
 .IP
+When a directory is specified, shows only an abbreviated status
+display for that directory. In this mode, all of the file matching
+options can be used to filter the files that will be included in
+the status.
+.IP
+For example, suppose you want to run "git annex get .", but
+would first like to see how much disk space that will use.
+Then run:
+.IP
+ git annex status . \-\-not \-\-in here
+.IP
 .IP "map"
 Helps you keep track of your repositories, and the connections between them,
 by going out and looking at all the ones it can get to, and generating a
@@ -781,9 +795,18 @@
 For example, to limit the bandwidth to 100Kbyte/s, set it to "\-\-bwlimit 100k"
 (There is no corresponding option for bup join.)
 .IP
-.IP "annex.ssh\-options, annex.rsync\-options, annex.bup\-split\-options"
-Default ssh, rsync, wget/curl, and bup options to use if a remote does not
-have specific options.
+.IP "remote.<name>.annex\-gnupg\-options"
+Options to pass to GnuPG for symmetric encryption. For instance, to
+use the AES cipher with a 256 bits key and disable compression, set it
+to "\-\-cipher\-algo AES256 \-\-compress\-algo none". (These options take
+precedence over the default GnuPG configuration, which is otherwise
+used.)
+.IP
+.IP "annex.ssh\-options, annex.rsync\-options, annex.bup\-split\-options,"
+annex.gnupg\-options
+.IP
+Default ssh, rsync, wget/curl, bup, and GnuPG options to use if a
+remote does not have specific options.
 .IP
 .IP "annex.web\-options"
 Options to use when using wget or curl to download a file from the web.
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: 4.20130227
+Version: 4.20130314
 Cabal-Version: >= 1.8
 License: GPL
 Maintainer: Joey Hess <joey@kitenet.net>
@@ -65,24 +65,25 @@
 Executable git-annex
   Main-Is: git-annex.hs
   Build-Depends: MissingH, hslogger, directory, filepath,
-   unix, containers, utf8-string, network (>= 2.0), mtl (>= 2.1.1),
+   unix, containers, utf8-string, network (>= 2.0), mtl (>= 2),
    bytestring, old-locale, time,
    extensible-exceptions, dataenc, SHA, process, json,
    base (>= 4.5 && < 4.8), monad-control, transformers-base, lifted-base,
    IfElse, text, QuickCheck >= 2.1, bloomfilter, edit-distance, process,
-   SafeSemaphore, uuid, random, regex-compat
+   SafeSemaphore, uuid, random, regex-tdfa
   -- Need to list these because they're generated from .hsc files.
   Other-Modules: Utility.Touch Utility.Mounts
   Include-Dirs: Utility
   C-Sources: Utility/libdiskfree.c Utility/libmounts.c
   CC-Options: -Wall
   GHC-Options: -threaded -Wall
+  CPP-Options: -DWITH_CLIBS
 
   if flag(Production)
     GHC-Options: -O2
 
   if flag(TestSuite)
-    Build-Depends: testpack, HUnit
+    Build-Depends: HUnit
     CPP-Options: -DWITH_TESTSUITE
 
   if flag(S3)
@@ -111,6 +112,7 @@
       if (! os(windows) && ! os(solaris) && ! os(linux))
         CPP-Options: -DWITH_KQUEUE
         C-Sources: Utility/libkqueue.c
+        Includes: sys/event.h
 
   if os(linux) && flag(Dbus)
     Build-Depends: dbus (>= 0.10.3)
diff --git a/standalone/android/Makefile b/standalone/android/Makefile
--- a/standalone/android/Makefile
+++ b/standalone/android/Makefile
@@ -11,35 +11,39 @@
 export ANDROID_SDK_ROOT?=$(HOME)/tmp/adt-bundle-linux-x86/sdk
 export ANDROID_NDK_ROOT?=$(HOME)/tmp/android-ndk-r8d
 
-GITTREE=source/git/installed-tree
+# Where to store the $(GIT_ANNEX_ANDROID_SOURCETREE)s to utilities. This
+# directory will be created by `make source`.
+GIT_ANNEX_ANDROID_SOURCETREE?=../tmp/android-sourcetree
 
+GITTREE=$(GIT_ANNEX_ANDROID_SOURCETREE)/git/installed-tree
+
 build: start
-	$(MAKE) source/openssl
-	$(MAKE) source/openssh
-	$(MAKE) source/busybox
-	$(MAKE) source/rsync
-	$(MAKE) source/gnupg
-	$(MAKE) source/git
-	$(MAKE) source/term
+	$(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/openssl
+	$(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh
+	$(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/busybox
+	$(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync
+	$(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg
+	$(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/git
+	$(MAKE) $(GIT_ANNEX_ANDROID_SOURCETREE)/term
 
 	# Debug build because it does not need signing keys.
-	cd source/term && tools/build-debug
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && tools/build-debug
 
 	# Install executables as pseudo-libraries so they will be
 	# unpacked from the .apk.
-	mkdir -p source/term/libs/armeabi
-	cp ../../git-annex source/term/libs/armeabi/lib.git-annex.so
-	cp source/busybox/busybox source/term/libs/armeabi/lib.busybox.so
-	cp source/openssh/ssh source/term/libs/armeabi/lib.ssh.so
-	cp source/openssh/ssh-keygen source/term/libs/armeabi/lib.ssh-keygen.so
-	cp source/rsync/rsync source/term/libs/armeabi/lib.rsync.so
-	cp source/gnupg/g10/gpg source/term/libs/armeabi/lib.gpg.so
-	cp source/git/git source/term/libs/armeabi/lib.git.so
-	cp source/git/git-shell source/term/libs/armeabi/lib.git-shell.so
-	cp source/git/git-upload-pack source/term/libs/armeabi/lib.git-upload-pack.so
-	arm-linux-androideabi-strip --strip-unneeded --remove-section=.comment --remove-section=.note source/term/libs/armeabi/*
-	cp runshell source/term/libs/armeabi/lib.runshell.so
-	cp start source/term/libs/armeabi/lib.start.so
+	mkdir -p $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi
+	cp ../../git-annex $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.git-annex.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/busybox/busybox $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.busybox.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh/ssh $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.ssh.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh/ssh-keygen $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.ssh-keygen.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync/rsync $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.rsync.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg/g10/gpg $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.gpg.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/git/git $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.git.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/git/git-shell $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.git-shell.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/git/git-upload-pack $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.git-upload-pack.so
+	arm-linux-androideabi-strip --strip-unneeded --remove-section=.comment --remove-section=.note $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/*
+	cp runshell $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.runshell.so
+	cp start $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.start.so
 	
 	# remove git stuff we don't need to save space
 	rm -rf $(GITTREE)/bin/git-cvsserver \
@@ -67,79 +71,83 @@
 	cd $(GITTREE) && find -samefile bin/git-shell -not -wholename ./bin/git-shell > links/git-shell
 	cd $(GITTREE) && find -samefile bin/git-upload-pack -not -wholename ./bin/git-upload-pack > links/git-upload-pack
 	cd $(GITTREE) && find -type f -not -samefile bin/git -not -samefile bin/git-shell -not -samefile bin/git-upload-pack|tar czf ../git.tar.gz -T -
-	cp source/git/git.tar.gz source/term/libs/armeabi/lib.git.tar.gz.so
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/git/git.tar.gz $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.git.tar.gz.so
 
-	git rev-parse HEAD > source/term/libs/armeabi/lib.version.so
+	git rev-parse HEAD > $(GIT_ANNEX_ANDROID_SOURCETREE)/term/libs/armeabi/lib.version.so
 
-	cd source/term && ant debug
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && ant debug
+	mkdir -p ../../tmp
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/term/bin/Term-debug.apk ../../tmp/git-annex.apk
 
-source/openssl:
-	cd source/openssl && CC=$$(which cc) ./Configure android
-	cd source/openssl && $(MAKE)
+$(GIT_ANNEX_ANDROID_SOURCETREE)/openssl:
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssl && CC=$$(which cc) ./Configure android
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssl && $(MAKE)
 	touch $@
 
-source/openssh: openssh.patch openssh.config.h
-	cd source/openssh && git reset --hard
-	cd source/openssh && ./configure --host=arm-linux-androideabi --with-ssl-dir=../openssl --without-openssl-header-check
-	cd source/openssh && patch -p1 < ../../openssh.patch
-	cp openssh.config.h source/openssh/config.h
-	cd source/openssh && sed -i -e 's/getrrsetbyname.o //' openbsd-compat/Makefile
-	cd source/openssh && sed -i -e 's/auth-passwd.o //' Makefile
-	cd source/openssh && $(MAKE) ssh ssh-keygen
+$(GIT_ANNEX_ANDROID_SOURCETREE)/openssh: openssh.patch openssh.config.h
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh && git reset --hard
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh && ./configure --host=arm-linux-androideabi --with-ssl-dir=../openssl --without-openssl-header-check
+	cat openssh.patch | (cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh && patch -p1)
+	cp openssh.config.h $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh/config.h
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh && sed -i -e 's/getrrsetbyname.o //' openbsd-compat/Makefile
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh && sed -i -e 's/auth-passwd.o //' Makefile
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh && $(MAKE) ssh ssh-keygen
 	touch $@
 
-source/busybox: busybox_config
-	cp busybox_config source/busybox/.config
-	cd source/busybox && yes '' | $(MAKE) oldconfig
-	cd source/busybox && $(MAKE)
+$(GIT_ANNEX_ANDROID_SOURCETREE)/busybox: busybox_config
+	cp busybox_config $(GIT_ANNEX_ANDROID_SOURCETREE)/busybox/.config
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/busybox && yes '' | $(MAKE) oldconfig
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/busybox && $(MAKE)
 	touch $@
 	
-source/git:
-	cd source/git && $(MAKE) install NO_OPENSSL=1 NO_GETTEXT=1 NO_GECOS_IN_PWENT=1 NO_GETPASS=1 NO_NSEC=1 NO_MKDTEMP=1 NO_PTHREADS=1 NO_PERL=1 NO_CURL=1 NO_EXPAT=1 NO_TCLTK=1 NO_ICONV=1 prefix= DESTDIR=installed-tree
+$(GIT_ANNEX_ANDROID_SOURCETREE)/git:
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/git && $(MAKE) install NO_OPENSSL=1 NO_GETTEXT=1 NO_GECOS_IN_PWENT=1 NO_GETPASS=1 NO_NSEC=1 NO_MKDTEMP=1 NO_PTHREADS=1 NO_PERL=1 NO_CURL=1 NO_EXPAT=1 NO_TCLTK=1 NO_ICONV=1 prefix= DESTDIR=installed-tree
 	touch $@
 
-source/rsync: rsync.patch
-	cd source/rsync && git reset --hard origin/master && git am < ../../rsync.patch
-	cp source/automake/lib/config.sub source/automake/lib/config.guess source/rsync/
-	cd source/rsync && ./configure --host=arm-linux-androideabi --disable-locale --disable-iconv-open --disable-iconv --disable-acl-support --disable-xattr-support
-	cd source/rsync && $(MAKE)
+$(GIT_ANNEX_ANDROID_SOURCETREE)/rsync: rsync.patch
+	cat rsync.patch | (cd $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync && git reset --hard origin/master && git am)
+	cp $(GIT_ANNEX_ANDROID_SOURCETREE)/automake/lib/config.sub $(GIT_ANNEX_ANDROID_SOURCETREE)/automake/lib/config.guess $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync/
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync && ./configure --host=arm-linux-androideabi --disable-locale --disable-iconv-open --disable-iconv --disable-acl-support --disable-xattr-support
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync && $(MAKE)
 	touch $@
 
-source/gnupg:
-	cd source/gnupg && git checkout gnupg-1.4.13
-	cd source/gnupg && ./autogen.sh
-	cd source/gnupg && ./configure --host=arm-linux-androideabi --disable-gnupg-iconv --enable-minimal --disable-card-support --disable-agent-support --disable-photo-viewers --disable-keyserver-helpers --disable-nls
-	cd source/gnupg; $(MAKE) || true # expected failure in doc build
+$(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg:
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg && git checkout gnupg-1.4.13
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg && ./autogen.sh
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg && ./configure --host=arm-linux-androideabi --disable-gnupg-iconv --enable-minimal --disable-card-support --disable-agent-support --disable-photo-viewers --disable-keyserver-helpers --disable-nls
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg; $(MAKE) || true # expected failure in doc build
 	touch $@
 
-source/term: term.patch icons
-	cd source/term && git reset --hard
-	cd source/term && patch -p1 <../../term.patch
-	(cd icons && tar c .) | (cd source/term/res && tar x)
+$(GIT_ANNEX_ANDROID_SOURCETREE)/term: term.patch icons
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && git reset --hard
+	cat term.patch | (cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && patch -p1)
+	(cd icons && tar c .) | (cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term/res && tar x)
 	# This renaming has a purpose. It makes the path to the app's
 	# /data directory shorter, which makes ssh connection caching
 	# sockets placed there have more space for their filenames.
 	# Also, it avoids overlap with the Android Terminal Emulator
 	# app, if it's also installed.
-	cd source/term && find -name .git -prune -o -type f -print0 | xargs -0 perl -pi -e 's/jackpal/ga/g'
-	cd source/term && perl -pi -e 's/Terminal Emulator/Git Annex/g' res/*/strings.xml
-	cd source/term && tools/update.sh >/dev/null 2>&1
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && find -name .git -prune -o -type f -print0 | xargs -0 perl -pi -e 's/jackpal/ga/g'
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && perl -pi -e 's/Terminal Emulator/Git Annex/g' res/*/strings.xml
+	cd $(GIT_ANNEX_ANDROID_SOURCETREE)/term && tools/update.sh >/dev/null 2>&1
 	touch $@
 
-source:
-	mkdir -p source
-	git clone --bare git://git.savannah.gnu.org/automake.git source/automake
-	git clone --bare git://git.debian.org/git/d-i/busybox source/busybox
-	git clone --bare git://git.kernel.org/pub/scm/git/git.git source/git
-	git clone --bare git://git.samba.org/rsync.git source/rsync
-	git clone --bare git://git.gnupg.org/gnupg.git source/gnupg
-	git clone --bare git://git.openssl.org/openssl source/openssl
-	git clone --bare git://github.com/CyanogenMod/android_external_openssh.git source/openssh
-	git clone --bare git://github.com/jackpal/Android-Terminal-Emulator.git source/term
+source: $(GIT_ANNEX_ANDROID_SOURCETREE)
 
+$(GIT_ANNEX_ANDROID_SOURCETREE):
+	mkdir -p $(GIT_ANNEX_ANDROID_SOURCETREE)
+	git clone --bare git://git.savannah.gnu.org/automake.git $(GIT_ANNEX_ANDROID_SOURCETREE)/automake
+	git clone --bare git://git.debian.org/git/d-i/busybox $(GIT_ANNEX_ANDROID_SOURCETREE)/busybox
+	git clone --bare git://git.kernel.org/pub/scm/git/git.git $(GIT_ANNEX_ANDROID_SOURCETREE)/git
+	git clone --bare git://git.samba.org/rsync.git $(GIT_ANNEX_ANDROID_SOURCETREE)/rsync
+	git clone --bare git://git.gnupg.org/gnupg.git $(GIT_ANNEX_ANDROID_SOURCETREE)/gnupg
+	git clone --bare git://git.openssl.org/openssl $(GIT_ANNEX_ANDROID_SOURCETREE)/openssl
+	git clone --bare git://github.com/CyanogenMod/android_external_openssh.git $(GIT_ANNEX_ANDROID_SOURCETREE)/openssh
+	git clone --bare git://github.com/jackpal/Android-Terminal-Emulator.git $(GIT_ANNEX_ANDROID_SOURCETREE)/term
+
 clean:
 	rm -rf $(GITTREE)
 	rm -f start
 
 reallyclean: clean
-	rm -rf source
+	rm -rf $(GIT_ANNEX_ANDROID_SOURCETREE)
diff --git a/standalone/android/haskell-patches/aeson-0.6.1.0_0001-disable-TH.patch b/standalone/android/haskell-patches/aeson-0.6.1.0_0001-disable-TH.patch
new file mode 100644
--- /dev/null
+++ b/standalone/android/haskell-patches/aeson-0.6.1.0_0001-disable-TH.patch
@@ -0,0 +1,24 @@
+From b220c377941d0b1271cf525a8d06bb8e48196d2b Mon Sep 17 00:00:00 2001
+From: Joey Hess <joey@kitenet.net>
+Date: Thu, 28 Feb 2013 23:29:04 -0400
+Subject: [PATCH] disable TH
+
+---
+ aeson.cabal |    1 -
+ 1 file changed, 1 deletion(-)
+
+diff --git a/aeson.cabal b/aeson.cabal
+index 242aa67..275aa49 100644
+--- a/aeson.cabal
++++ b/aeson.cabal
+@@ -99,7 +99,6 @@ library
+     Data.Aeson.Generic
+     Data.Aeson.Parser
+     Data.Aeson.Types
+-    Data.Aeson.TH
+ 
+   other-modules:
+     Data.Aeson.Functions
+-- 
+1.7.10.4
+
diff --git a/standalone/android/haskell-patches/hamlet-1.1.6.1_0001-axe-murdered.patch b/standalone/android/haskell-patches/hamlet-1.1.6.1_0001-axe-murdered.patch
new file mode 100644
--- /dev/null
+++ b/standalone/android/haskell-patches/hamlet-1.1.6.1_0001-axe-murdered.patch
@@ -0,0 +1,276 @@
+From 7be8bf3ba75acc5209066e6ba31ae589c541f344 Mon Sep 17 00:00:00 2001
+From: Joey Hess <joey@kitenet.net>
+Date: Thu, 28 Feb 2013 23:30:01 -0400
+Subject: [PATCH] axe murdered
+
+---
+ Text/Hamlet.hs |  215 +-------------------------------------------------------
+ 1 file changed, 2 insertions(+), 213 deletions(-)
+
+diff --git a/Text/Hamlet.hs b/Text/Hamlet.hs
+index 4ac870a..bc8edd5 100644
+--- a/Text/Hamlet.hs
++++ b/Text/Hamlet.hs
+@@ -11,35 +11,22 @@
+ module Text.Hamlet
+     ( -- * Plain HTML
+       Html
+-    , shamlet
+-    , shamletFile
+-    , xshamlet
+-    , xshamletFile
+       -- * Hamlet
+     , HtmlUrl
+-    , hamlet
+-    , hamletFile
+-    , xhamlet
+-    , xhamletFile
+       -- * I18N Hamlet
+     , HtmlUrlI18n
+-    , ihamlet
+-    , ihamletFile
+       -- * Type classes
+     , ToAttributes (..)
+       -- * Internal, for making more
+     , HamletSettings (..)
+     , NewlineStyle (..)
+-    , hamletWithSettings
+-    , hamletFileWithSettings
+     , defaultHamletSettings
+     , xhtmlHamletSettings
+     , Env (..)
+     , HamletRules (..)
+-    , hamletRules
+-    , ihamletRules
+-    , htmlRules
+     , CloseStyle (..)
++    , condH
++    , maybeH
+     ) where
+ 
+ import Text.Shakespeare.Base
+@@ -90,14 +77,6 @@ type HtmlUrl url = Render url -> Html
+ -- | A function generating an 'Html' given a message translator and a URL rendering function.
+ type HtmlUrlI18n msg url = Translate msg -> Render url -> Html
+ 
+-docsToExp :: Env -> HamletRules -> Scope -> [Doc] -> Q Exp
+-docsToExp env hr scope docs = do
+-    exps <- mapM (docToExp env hr scope) docs
+-    case exps of
+-        [] -> [|return ()|]
+-        [x] -> return x
+-        _ -> return $ DoE $ map NoBindS exps
+-
+ unIdent :: Ident -> String
+ unIdent (Ident s) = s
+ 
+@@ -159,169 +138,9 @@ recordToFieldNames conStr = do
+   [fields] <- return [fields | RecC name fields <- cons, name == conName]
+   return [fieldName | (fieldName, _, _) <- fields]
+ 
+-docToExp :: Env -> HamletRules -> Scope -> Doc -> Q Exp
+-docToExp env hr scope (DocForall list idents inside) = do
+-    let list' = derefToExp scope list
+-    (pat, extraScope) <- bindingPattern idents
+-    let scope' = extraScope ++ scope
+-    mh <- [|F.mapM_|]
+-    inside' <- docsToExp env hr scope' inside
+-    let lam = LamE [pat] inside'
+-    return $ mh `AppE` lam `AppE` list'
+-docToExp env hr scope (DocWith [] inside) = do
+-    inside' <- docsToExp env hr scope inside
+-    return $ inside'
+-docToExp env hr scope (DocWith ((deref, idents):dis) inside) = do
+-    let deref' = derefToExp scope deref
+-    (pat, extraScope) <- bindingPattern idents
+-    let scope' = extraScope ++ scope
+-    inside' <- docToExp env hr scope' (DocWith dis inside)
+-    let lam = LamE [pat] inside'
+-    return $ lam `AppE` deref'
+-docToExp env hr scope (DocMaybe val idents inside mno) = do
+-    let val' = derefToExp scope val
+-    (pat, extraScope) <- bindingPattern idents
+-    let scope' = extraScope ++ scope
+-    inside' <- docsToExp env hr scope' inside
+-    let inside'' = LamE [pat] inside'
+-    ninside' <- case mno of
+-                    Nothing -> [|Nothing|]
+-                    Just no -> do
+-                        no' <- docsToExp env hr scope no
+-                        j <- [|Just|]
+-                        return $ j `AppE` no'
+-    mh <- [|maybeH|]
+-    return $ mh `AppE` val' `AppE` inside'' `AppE` ninside'
+-docToExp env hr scope (DocCond conds final) = do
+-    conds' <- mapM go conds
+-    final' <- case final of
+-                Nothing -> [|Nothing|]
+-                Just f -> do
+-                    f' <- docsToExp env hr scope f
+-                    j <- [|Just|]
+-                    return $ j `AppE` f'
+-    ch <- [|condH|]
+-    return $ ch `AppE` ListE conds' `AppE` final'
+-  where
+-    go :: (Deref, [Doc]) -> Q Exp
+-    go (d, docs) = do
+-        let d' = derefToExp scope d
+-        docs' <- docsToExp env hr scope docs
+-        return $ TupE [d', docs']
+-docToExp env hr scope (DocCase deref cases) = do
+-    let exp_ = derefToExp scope deref
+-    matches <- mapM toMatch cases
+-    return $ CaseE exp_ matches
+-  where
+-    readMay s =
+-        case reads s of
+-            (x, ""):_ -> Just x
+-            _ -> Nothing
+-    toMatch (idents, inside) = do
+-        let pat = case map unIdent idents of
+-                    ["_"] -> WildP
+-                    [str]
+-                        | Just i <- readMay str -> LitP $ IntegerL i
+-                    strs -> let (constr:fields) = map mkName strs
+-                            in ConP constr (map VarP fields)
+-        insideExp <- docsToExp env hr scope inside
+-        return $ Match pat (NormalB insideExp) []
+-docToExp env hr v (DocContent c) = contentToExp env hr v c
+-
+-contentToExp :: Env -> HamletRules -> Scope -> Content -> Q Exp
+-contentToExp _ hr _ (ContentRaw s) = do
+-    os <- [|preEscapedText . pack|]
+-    let s' = LitE $ StringL s
+-    return $ hrFromHtml hr `AppE` (os `AppE` s')
+-contentToExp _ hr scope (ContentVar d) = do
+-    str <- [|toHtml|]
+-    return $ hrFromHtml hr `AppE` (str `AppE` derefToExp scope d)
+-contentToExp env hr scope (ContentUrl hasParams d) =
+-    case urlRender env of
+-        Nothing -> error "URL interpolation used, but no URL renderer provided"
+-        Just wrender -> wrender $ \render -> do
+-            let render' = return render
+-            ou <- if hasParams
+-                    then [|\(u, p) -> $(render') u p|]
+-                    else [|\u -> $(render') u []|]
+-            let d' = derefToExp scope d
+-            pet <- [|toHtml|]
+-            return $ hrFromHtml hr `AppE` (pet `AppE` (ou `AppE` d'))
+-contentToExp env hr scope (ContentEmbed d) = hrEmbed hr env $ derefToExp scope d
+-contentToExp env hr scope (ContentMsg d) =
+-    case msgRender env of
+-        Nothing -> error "Message interpolation used, but no message renderer provided"
+-        Just wrender -> wrender $ \render ->
+-            return $ hrFromHtml hr `AppE` (render `AppE` derefToExp scope d)
+-contentToExp _ hr scope (ContentAttrs d) = do
+-    html <- [|attrsToHtml . toAttributes|]
+-    return $ hrFromHtml hr `AppE` (html `AppE` derefToExp scope d)
+-
+-shamlet :: QuasiQuoter
+-shamlet = hamletWithSettings htmlRules defaultHamletSettings
+-
+-xshamlet :: QuasiQuoter
+-xshamlet = hamletWithSettings htmlRules xhtmlHamletSettings
+-
+-htmlRules :: Q HamletRules
+-htmlRules = do
+-    i <- [|id|]
+-    return $ HamletRules i ($ (Env Nothing Nothing)) (\_ b -> return b)
+-
+-hamlet :: QuasiQuoter
+-hamlet = hamletWithSettings hamletRules defaultHamletSettings
+-
+-xhamlet :: QuasiQuoter
+-xhamlet = hamletWithSettings hamletRules xhtmlHamletSettings
+-
+ asHtmlUrl :: HtmlUrl url -> HtmlUrl url
+ asHtmlUrl = id
+ 
+-hamletRules :: Q HamletRules
+-hamletRules = do
+-    i <- [|id|]
+-    let ur f = do
+-            r <- newName "_render"
+-            let env = Env
+-                    { urlRender = Just ($ (VarE r))
+-                    , msgRender = Nothing
+-                    }
+-            h <- f env
+-            return $ LamE [VarP r] h
+-    return $ HamletRules i ur em
+-  where
+-    em (Env (Just urender) Nothing) e = do
+-        asHtmlUrl' <- [|asHtmlUrl|]
+-        urender $ \ur' -> return ((asHtmlUrl' `AppE` e) `AppE` ur')
+-    em _ _ = error "bad Env"
+-
+-ihamlet :: QuasiQuoter
+-ihamlet = hamletWithSettings ihamletRules defaultHamletSettings
+-
+-ihamletRules :: Q HamletRules
+-ihamletRules = do
+-    i <- [|id|]
+-    let ur f = do
+-            u <- newName "_urender"
+-            m <- newName "_mrender"
+-            let env = Env
+-                    { urlRender = Just ($ (VarE u))
+-                    , msgRender = Just ($ (VarE m))
+-                    }
+-            h <- f env
+-            return $ LamE [VarP m, VarP u] h
+-    return $ HamletRules i ur em
+-  where
+-    em (Env (Just urender) (Just mrender)) e =
+-          urender $ \ur' -> mrender $ \mr -> return (e `AppE` mr `AppE` ur')
+-    em _ _ = error "bad Env"
+-
+-hamletWithSettings :: Q HamletRules -> HamletSettings -> QuasiQuoter
+-hamletWithSettings hr set =
+-    QuasiQuoter
+-        { quoteExp = hamletFromString hr set
+-        }
+-
+ data HamletRules = HamletRules
+     { hrFromHtml :: Exp
+     , hrWithEnv :: (Env -> Q Exp) -> Q Exp
+@@ -333,36 +152,6 @@ data Env = Env
+     , msgRender :: Maybe ((Exp -> Q Exp) -> Q Exp)
+     }
+ 
+-hamletFromString :: Q HamletRules -> HamletSettings -> String -> Q Exp
+-hamletFromString qhr set s = do
+-    hr <- qhr
+-    case parseDoc set s of
+-        Error s' -> error s'
+-        Ok (_mnl, d) -> hrWithEnv hr $ \env -> docsToExp env hr [] d
+-
+-hamletFileWithSettings :: Q HamletRules -> HamletSettings -> FilePath -> Q Exp
+-hamletFileWithSettings qhr set fp = do
+-#ifdef GHC_7_4
+-    qAddDependentFile fp
+-#endif
+-    contents <- fmap TL.unpack $ qRunIO $ readUtf8File fp
+-    hamletFromString qhr set contents
+-
+-hamletFile :: FilePath -> Q Exp
+-hamletFile = hamletFileWithSettings hamletRules defaultHamletSettings
+-
+-xhamletFile :: FilePath -> Q Exp
+-xhamletFile = hamletFileWithSettings hamletRules xhtmlHamletSettings
+-
+-shamletFile :: FilePath -> Q Exp
+-shamletFile = hamletFileWithSettings htmlRules defaultHamletSettings
+-
+-xshamletFile :: FilePath -> Q Exp
+-xshamletFile = hamletFileWithSettings htmlRules xhtmlHamletSettings
+-
+-ihamletFile :: FilePath -> Q Exp
+-ihamletFile = hamletFileWithSettings ihamletRules defaultHamletSettings
+-
+ varName :: Scope -> String -> Exp
+ varName _ "" = error "Illegal empty varName"
+ varName scope v@(_:_) = fromMaybe (strToExp v) $ lookup (Ident v) scope
+-- 
+1.7.10.4
+
diff --git a/standalone/android/haskell-patches/persistent-1.1.5.1_0001-disable-TH.patch b/standalone/android/haskell-patches/persistent-1.1.5.1_0001-disable-TH.patch
new file mode 100644
--- /dev/null
+++ b/standalone/android/haskell-patches/persistent-1.1.5.1_0001-disable-TH.patch
@@ -0,0 +1,71 @@
+From 8fddef803ee9191ca15363283b7e4d5af4c70f3a Mon Sep 17 00:00:00 2001
+From: Joey Hess <joey@kitenet.net>
+Date: Thu, 28 Feb 2013 23:34:10 -0400
+Subject: [PATCH] disable TH
+
+---
+ Database/Persist/GenericSql/Internal.hs |    6 +-----
+ Database/Persist/GenericSql/Raw.hs      |    5 ++---
+ 2 files changed, 3 insertions(+), 8 deletions(-)
+
+diff --git a/Database/Persist/GenericSql/Internal.hs b/Database/Persist/GenericSql/Internal.hs
+index f109887..5273398 100644
+--- a/Database/Persist/GenericSql/Internal.hs
++++ b/Database/Persist/GenericSql/Internal.hs
+@@ -14,7 +14,6 @@ module Database.Persist.GenericSql.Internal
+     , createSqlPool
+     , mkColumns
+     , Column (..)
+-    , logSQL
+     , InsertSqlResult (..)
+     ) where
+ 
+@@ -33,7 +32,7 @@ import Data.Monoid (Monoid, mappend, mconcat)
+ import Database.Persist.EntityDef
+ import qualified Data.Conduit as C
+ import Language.Haskell.TH.Syntax (Q, Exp)
+-import Control.Monad.Logger (logDebugS)
++
+ import Data.Maybe (mapMaybe, listToMaybe)
+ import Data.Int (Int64)
+ 
+@@ -197,6 +196,3 @@ tableColumn t s = go $ entityColumns t
+         | x == s = ColumnDef x y z
+         | otherwise = go rest
+ -}
+-
+-logSQL :: Q Exp
+-logSQL = [|\sql_foo params_foo -> $logDebugS (T.pack "SQL") $ T.pack $ show (sql_foo :: Text) ++ " " ++ show (params_foo :: [PersistValue])|]
+diff --git a/Database/Persist/GenericSql/Raw.hs b/Database/Persist/GenericSql/Raw.hs
+index e4bf9f4..3da8fa0 100644
+--- a/Database/Persist/GenericSql/Raw.hs
++++ b/Database/Persist/GenericSql/Raw.hs
+@@ -26,7 +26,6 @@ import Database.Persist.GenericSql.Internal hiding (execute, withStmt)
+ import Database.Persist.Store (PersistValue)
+ import Data.IORef
+ import Control.Monad.IO.Class
+-import Control.Monad.Logger (logDebugS)
+ import Control.Monad.Trans.Reader
+ import qualified Data.Map as Map
+ import Control.Applicative (Applicative)
+@@ -134,7 +133,7 @@ withStmt :: (MonadSqlPersist m, MonadResource m)
+          -> [PersistValue]
+          -> Source m [PersistValue]
+ withStmt sql vals = do
+-    lift $ $logDebugS (pack "SQL") $ pack $ show sql ++ " " ++ show vals
++    -- lift $  pack $ show sql ++ " " ++ show vals
+     conn <- lift askSqlConn
+     bracketP
+         (getStmt' conn sql)
+@@ -146,7 +145,7 @@ execute x y = liftM (const ()) $ executeCount x y
+ 
+ executeCount :: MonadSqlPersist m => Text -> [PersistValue] -> m Int64
+ executeCount sql vals = do
+-    $logDebugS (pack "SQL") $ pack $ show sql ++ " " ++ show vals
++    -- pack $ show sql ++ " " ++ show vals
+     stmt <- getStmt sql
+     res <- liftIO $ I.execute stmt vals
+     liftIO $ reset stmt
+-- 
+1.7.10.4
+
diff --git a/standalone/android/haskell-patches/shakespeare-1.0.3_0001-remove-TH.patch b/standalone/android/haskell-patches/shakespeare-1.0.3_0001-remove-TH.patch
new file mode 100644
--- /dev/null
+++ b/standalone/android/haskell-patches/shakespeare-1.0.3_0001-remove-TH.patch
@@ -0,0 +1,194 @@
+From 2e6721d571148cb77fb8c906042f6fa61e660999 Mon Sep 17 00:00:00 2001
+From: Joey Hess <joey@kitenet.net>
+Date: Thu, 28 Feb 2013 23:35:41 -0400
+Subject: [PATCH] remove TH
+
+---
+ Text/Shakespeare.hs      |  109 ----------------------------------------------
+ Text/Shakespeare/Base.hs |   28 ------------
+ 2 files changed, 137 deletions(-)
+
+diff --git a/Text/Shakespeare.hs b/Text/Shakespeare.hs
+index e774e65..d300951 100644
+--- a/Text/Shakespeare.hs
++++ b/Text/Shakespeare.hs
+@@ -12,11 +12,7 @@ module Text.Shakespeare
+     , WrapInsertion (..)
+     , PreConversion (..)
+     , defaultShakespeareSettings
+-    , shakespeare
+-    , shakespeareFile
+-    , shakespeareFileReload
+     -- * low-level
+-    , shakespeareFromString
+     , shakespeareUsedIdentifiers
+     , RenderUrl
+     , VarType
+@@ -133,39 +129,6 @@ defaultShakespeareSettings = ShakespeareSettings {
+   , modifyFinalValue = Nothing
+ }
+ 
+-instance Lift PreConvert where
+-    lift (PreConvert convert ignore comment wrapInsertion) =
+-        [|PreConvert $(lift convert) $(lift ignore) $(lift comment) $(lift wrapInsertion)|]
+-
+-instance Lift WrapInsertion where
+-    lift (WrapInsertion indent sb sep sc e ab ac) =
+-        [|WrapInsertion $(lift indent) $(lift sb) $(lift sep) $(lift sc) $(lift e) $(lift ab) $(lift ac)|]
+-
+-instance Lift PreConversion where
+-    lift (ReadProcess command args) =
+-        [|ReadProcess $(lift command) $(lift args)|]
+-    lift Id = [|Id|]
+-
+-instance Lift ShakespeareSettings where
+-    lift (ShakespeareSettings x1 x2 x3 x4 x5 x6 x7 x8 x9) =
+-        [|ShakespeareSettings
+-            $(lift x1) $(lift x2) $(lift x3)
+-            $(liftExp x4) $(liftExp x5) $(liftExp x6) $(lift x7) $(lift x8) $(liftMExp x9)|]
+-      where
+-        liftExp (VarE n) = [|VarE $(liftName n)|]
+-        liftExp (ConE n) = [|ConE $(liftName n)|]
+-        liftExp _ = error "liftExp only supports VarE and ConE"
+-        liftMExp Nothing = [|Nothing|]
+-        liftMExp (Just e) = [|Just|] `appE` liftExp e
+-        liftName (Name (OccName a) b) = [|Name (OccName $(lift a)) $(liftFlavour b)|]
+-        liftFlavour NameS = [|NameS|]
+-        liftFlavour (NameQ (ModName a)) = [|NameQ (ModName $(lift a))|]
+-        liftFlavour (NameU _) = error "liftFlavour NameU" -- [|NameU $(lift $ fromIntegral a)|]
+-        liftFlavour (NameL _) = error "liftFlavour NameL" -- [|NameU $(lift $ fromIntegral a)|]
+-        liftFlavour (NameG ns (PkgName p) (ModName m)) = [|NameG $(liftNS ns) (PkgName $(lift p)) (ModName $(lift m))|]
+-        liftNS VarName = [|VarName|]
+-        liftNS DataName = [|DataName|]
+-
+ type QueryParameters = [(TS.Text, TS.Text)]
+ type RenderUrl url = (url -> QueryParameters -> TS.Text)
+ type Shakespeare url = RenderUrl url -> Builder
+@@ -300,54 +263,6 @@ pack' = TS.pack
+ {-# NOINLINE pack' #-}
+ #endif
+ 
+-contentsToShakespeare :: ShakespeareSettings -> [Content] -> Q Exp
+-contentsToShakespeare rs a = do
+-    r <- newName "_render"
+-    c <- mapM (contentToBuilder r) a
+-    compiledTemplate <- case c of
+-        -- Make sure we convert this mempty using toBuilder to pin down the
+-        -- type appropriately
+-        []  -> fmap (AppE $ wrap rs) [|mempty|]
+-        [x] -> return x
+-        _   -> do
+-              mc <- [|mconcat|]
+-              return $ mc `AppE` ListE c
+-    fmap (maybe id AppE $ modifyFinalValue rs) $
+-        if justVarInterpolation rs
+-            then return compiledTemplate
+-            else return $ LamE [VarP r] compiledTemplate
+-      where
+-        contentToBuilder :: Name -> Content -> Q Exp
+-        contentToBuilder _ (ContentRaw s') = do
+-            ts <- [|fromText . pack'|]
+-            return $ wrap rs `AppE` (ts `AppE` LitE (StringL s'))
+-        contentToBuilder _ (ContentVar d) =
+-            return $ wrap rs `AppE` (toBuilder rs `AppE` derefToExp [] d)
+-        contentToBuilder r (ContentUrl d) = do
+-            ts <- [|fromText|]
+-            return $ wrap rs `AppE` (ts `AppE` (VarE r `AppE` derefToExp [] d `AppE` ListE []))
+-        contentToBuilder r (ContentUrlParam d) = do
+-            ts <- [|fromText|]
+-            up <- [|\r' (u, p) -> r' u p|]
+-            return $ wrap rs `AppE` (ts `AppE` (up `AppE` VarE r `AppE` derefToExp [] d))
+-        contentToBuilder r (ContentMix d) =
+-            return $ derefToExp [] d `AppE` VarE r
+-
+-shakespeare :: ShakespeareSettings -> QuasiQuoter
+-shakespeare r = QuasiQuoter { quoteExp = shakespeareFromString r }
+-
+-shakespeareFromString :: ShakespeareSettings -> String -> Q Exp
+-shakespeareFromString r str = do
+-    s <- qRunIO $ preFilter r str
+-    contentsToShakespeare r $ contentFromString r s
+-
+-shakespeareFile :: ShakespeareSettings -> FilePath -> Q Exp
+-shakespeareFile r fp = do
+-#ifdef GHC_7_4
+-    qAddDependentFile fp
+-#endif
+-    readFileQ fp >>= shakespeareFromString r
+-
+ data VarType = VTPlain | VTUrl | VTUrlParam | VTMixin
+ 
+ getVars :: Content -> [(Deref, VarType)]
+@@ -367,30 +282,6 @@ data VarExp url = EPlain Builder
+ shakespeareUsedIdentifiers :: ShakespeareSettings -> String -> [(Deref, VarType)]
+ shakespeareUsedIdentifiers settings = concatMap getVars . contentFromString settings
+ 
+-shakespeareFileReload :: ShakespeareSettings -> FilePath -> Q Exp
+-shakespeareFileReload rs fp = do
+-    str <- readFileQ fp
+-    s <- qRunIO $ preFilter rs str
+-    let b = shakespeareUsedIdentifiers rs s
+-    c <- mapM vtToExp b
+-    rt <- [|shakespeareRuntime|]
+-    wrap' <- [|\x -> $(return $ wrap rs) . x|]
+-    r' <- lift rs
+-    return $ wrap' `AppE` (rt `AppE` r' `AppE` (LitE $ StringL fp) `AppE` ListE c)
+-  where
+-    vtToExp :: (Deref, VarType) -> Q Exp
+-    vtToExp (d, vt) = do
+-        d' <- lift d
+-        c' <- c vt
+-        return $ TupE [d', c' `AppE` derefToExp [] d]
+-      where
+-        c :: VarType -> Q Exp
+-        c VTPlain = [|EPlain . $(return $ toBuilder rs)|]
+-        c VTUrl = [|EUrl|]
+-        c VTUrlParam = [|EUrlParam|]
+-        c VTMixin = [|\x -> EMixin $ \r -> $(return $ unwrap rs) $ x r|]
+-
+-
+ shakespeareRuntime :: ShakespeareSettings -> FilePath -> [(Deref, VarExp url)] -> Shakespeare url
+ shakespeareRuntime rs fp cd render' = unsafePerformIO $ do
+     str <- readFileUtf8 fp
+diff --git a/Text/Shakespeare/Base.hs b/Text/Shakespeare/Base.hs
+index 7c96898..ef769b1 100644
+--- a/Text/Shakespeare/Base.hs
++++ b/Text/Shakespeare/Base.hs
+@@ -52,34 +52,6 @@ data Deref = DerefModulesIdent [String] Ident
+            | DerefTuple [Deref]
+     deriving (Show, Eq, Read, Data, Typeable, Ord)
+ 
+-instance Lift Ident where
+-    lift (Ident s) = [|Ident|] `appE` lift s
+-instance Lift Deref where
+-    lift (DerefModulesIdent v s) = do
+-        dl <- [|DerefModulesIdent|]
+-        v' <- lift v
+-        s' <- lift s
+-        return $ dl `AppE` v' `AppE` s'
+-    lift (DerefIdent s) = do
+-        dl <- [|DerefIdent|]
+-        s' <- lift s
+-        return $ dl `AppE` s'
+-    lift (DerefBranch x y) = do
+-        x' <- lift x
+-        y' <- lift y
+-        db <- [|DerefBranch|]
+-        return $ db `AppE` x' `AppE` y'
+-    lift (DerefIntegral i) = [|DerefIntegral|] `appE` lift i
+-    lift (DerefRational r) = do
+-        n <- lift $ numerator r
+-        d <- lift $ denominator r
+-        per <- [|(%) :: Int -> Int -> Ratio Int|]
+-        dr <- [|DerefRational|]
+-        return $ dr `AppE` InfixE (Just n) per (Just d)
+-    lift (DerefString s) = [|DerefString|] `appE` lift s
+-    lift (DerefList x) = [|DerefList $(lift x)|]
+-    lift (DerefTuple x) = [|DerefTuple $(lift x)|]
+-
+ derefParens, derefCurlyBrackets :: UserParser a Deref
+ derefParens        = between (char '(') (char ')') parseDeref
+ derefCurlyBrackets = between (char '{') (char '}') parseDeref
+-- 
+1.7.10.4
+
diff --git a/standalone/android/haskell-patches/shakespeare-js-1.1.2_0001-remove-TH.patch b/standalone/android/haskell-patches/shakespeare-js-1.1.2_0001-remove-TH.patch
new file mode 100644
--- /dev/null
+++ b/standalone/android/haskell-patches/shakespeare-js-1.1.2_0001-remove-TH.patch
@@ -0,0 +1,303 @@
+From f3e31696cfb45a528e4b4b6f016dc7101d7cd4fb Mon Sep 17 00:00:00 2001
+From: Joey Hess <joey@kitenet.net>
+Date: Thu, 28 Feb 2013 23:36:06 -0400
+Subject: [PATCH] remove TH
+
+---
+ Text/Coffee.hs     |   54 -------------------------------------------------
+ Text/Julius.hs     |   53 +-----------------------------------------------
+ Text/Roy.hs        |   54 -------------------------------------------------
+ Text/TypeScript.hs |   57 +---------------------------------------------------
+ 4 files changed, 2 insertions(+), 216 deletions(-)
+
+diff --git a/Text/Coffee.hs b/Text/Coffee.hs
+index 2481936..3f7f9c3 100644
+--- a/Text/Coffee.hs
++++ b/Text/Coffee.hs
+@@ -51,14 +51,6 @@ module Text.Coffee
+       -- ** Template-Reading Functions
+       -- | These QuasiQuoter and Template Haskell methods return values of
+       -- type @'JavascriptUrl' url@. See the Yesod book for details.
+-      coffee
+-    , coffeeFile
+-    , coffeeFileReload
+-    , coffeeFileDebug
+-
+-#ifdef TEST_EXPORT
+-    , coffeeSettings
+-#endif
+     ) where
+ 
+ import Language.Haskell.TH.Quote (QuasiQuoter (..))
+@@ -66,49 +58,3 @@ import Language.Haskell.TH.Syntax
+ import Text.Shakespeare
+ import Text.Julius
+ 
+-coffeeSettings :: Q ShakespeareSettings
+-coffeeSettings = do
+-  jsettings <- javascriptSettings
+-  return $ jsettings { varChar = '%'
+-  , preConversion = Just PreConvert {
+-      preConvert = ReadProcess "coffee" ["-spb"]
+-    , preEscapeIgnoreBalanced = "'\"`"     -- don't insert backtacks for variable already inside strings or backticks.
+-    , preEscapeIgnoreLine = "#"            -- ignore commented lines
+-    , wrapInsertion = Just WrapInsertion { 
+-        wrapInsertionIndent = Just "  "
+-      , wrapInsertionStartBegin = "(("
+-      , wrapInsertionSeparator = ", "
+-      , wrapInsertionStartClose = ") =>"
+-      , wrapInsertionEnd = ")"
+-      , wrapInsertionApplyBegin = "("
+-      , wrapInsertionApplyClose = ")\n"
+-      }
+-    }
+-  }
+-
+--- | Read inline, quasiquoted CoffeeScript.
+-coffee :: QuasiQuoter
+-coffee = QuasiQuoter { quoteExp = \s -> do
+-    rs <- coffeeSettings
+-    quoteExp (shakespeare rs) s
+-    }
+-
+--- | Read in a CoffeeScript template file. This function reads the file once, at
+--- compile time.
+-coffeeFile :: FilePath -> Q Exp
+-coffeeFile fp = do
+-    rs <- coffeeSettings
+-    shakespeareFile rs fp
+-
+--- | Read in a CoffeeScript template file. This impure function uses
+--- unsafePerformIO to re-read the file on every call, allowing for rapid
+--- iteration.
+-coffeeFileReload :: FilePath -> Q Exp
+-coffeeFileReload fp = do
+-    rs <- coffeeSettings
+-    shakespeareFileReload rs fp
+-
+--- | Deprecated synonym for 'coffeeFileReload'
+-coffeeFileDebug :: FilePath -> Q Exp
+-coffeeFileDebug = coffeeFileReload
+-{-# DEPRECATED coffeeFileDebug "Please use coffeeFileReload instead." #-}
+diff --git a/Text/Julius.hs b/Text/Julius.hs
+index 230eac3..b990f73 100644
+--- a/Text/Julius.hs
++++ b/Text/Julius.hs
+@@ -14,17 +14,8 @@ module Text.Julius
+       -- ** Template-Reading Functions
+       -- | These QuasiQuoter and Template Haskell methods return values of
+       -- type @'JavascriptUrl' url@. See the Yesod book for details.
+-      js
+-    , julius
+-    , juliusFile
+-    , jsFile
+-    , juliusFileDebug
+-    , jsFileDebug
+-    , juliusFileReload
+-    , jsFileReload
+-
+       -- * Datatypes
+-    , JavascriptUrl
++      JavascriptUrl
+     , Javascript (..)
+     , RawJavascript (..)
+ 
+@@ -37,7 +28,6 @@ module Text.Julius
+     , renderJavascriptUrl
+ 
+       -- ** internal, used by 'Text.Coffee'
+-    , javascriptSettings
+       -- ** internal
+     , juliusUsedIdentifiers
+     ) where
+@@ -101,47 +91,6 @@ instance RawJS TL.Text where rawJS = RawJavascript . fromLazyText
+ instance RawJS Builder where rawJS = RawJavascript
+ instance RawJS Bool where rawJS = RawJavascript . toJavascript
+ 
+-javascriptSettings :: Q ShakespeareSettings
+-javascriptSettings = do
+-  toJExp <- [|toJavascript|]
+-  wrapExp <- [|Javascript|]
+-  unWrapExp <- [|unJavascript|]
+-  asJavascriptUrl' <- [|asJavascriptUrl|]
+-  return $ defaultShakespeareSettings { toBuilder = toJExp
+-  , wrap = wrapExp
+-  , unwrap = unWrapExp
+-  , modifyFinalValue = Just asJavascriptUrl'
+-  }
+-
+-js, julius :: QuasiQuoter
+-js = QuasiQuoter { quoteExp = \s -> do
+-    rs <- javascriptSettings
+-    quoteExp (shakespeare rs) s
+-    }
+-
+-julius = js
+-
+-jsFile, juliusFile :: FilePath -> Q Exp
+-jsFile fp = do
+-    rs <- javascriptSettings
+-    shakespeareFile rs fp
+-
+-juliusFile = jsFile
+-
+-
+-jsFileReload, juliusFileReload :: FilePath -> Q Exp
+-jsFileReload fp = do
+-    rs <- javascriptSettings
+-    shakespeareFileReload rs fp
+-
+-juliusFileReload = jsFileReload
+-
+-jsFileDebug, juliusFileDebug :: FilePath -> Q Exp
+-juliusFileDebug = jsFileReload
+-{-# DEPRECATED juliusFileDebug "Please use juliusFileReload instead." #-}
+-jsFileDebug = jsFileReload
+-{-# DEPRECATED jsFileDebug "Please use jsFileReload instead." #-}
+-
+ -- | Determine which identifiers are used by the given template, useful for
+ -- creating systems like yesod devel.
+ juliusUsedIdentifiers :: String -> [(Deref, VarType)]
+diff --git a/Text/Roy.hs b/Text/Roy.hs
+index cf09cec..870c9f6 100644
+--- a/Text/Roy.hs
++++ b/Text/Roy.hs
+@@ -23,13 +23,6 @@ module Text.Roy
+       -- ** Template-Reading Functions
+       -- | These QuasiQuoter and Template Haskell methods return values of
+       -- type @'JavascriptUrl' url@. See the Yesod book for details.
+-      roy
+-    , royFile
+-    , royFileReload
+-
+-#ifdef TEST_EXPORT
+-    , roySettings
+-#endif
+     ) where
+ 
+ import Language.Haskell.TH.Quote (QuasiQuoter (..))
+@@ -37,50 +30,3 @@ import Language.Haskell.TH.Syntax
+ import Text.Shakespeare
+ import Text.Julius
+ 
+--- | The Roy language compiles down to Javascript.
+--- We do this compilation once at compile time to avoid needing to do it during the request.
+--- We call this a preConversion because other shakespeare modules like Lucius use Haskell to compile during the request instead rather than a system call.
+-roySettings :: Q ShakespeareSettings
+-roySettings = do
+-  jsettings <- javascriptSettings
+-  return $ jsettings { varChar = '#'
+-  , preConversion = Just PreConvert {
+-      preConvert = ReadProcess "roy" ["--stdio"]
+-    , preEscapeIgnoreBalanced = "'\""
+-    , preEscapeIgnoreLine = "//"
+-    , wrapInsertion = Nothing
+-    {-
+-    Just WrapInsertion { 
+-        wrapInsertionIndent = Just "  "
+-      , wrapInsertionStartBegin = "(\\"
+-      , wrapInsertionSeparator = " "
+-      , wrapInsertionStartClose = " ->\n"
+-      , wrapInsertionEnd = ")"
+-      , wrapInsertionApplyBegin = " "
+-      , wrapInsertionApplyClose = ")\n"
+-      }
+-      -}
+-    }
+-  }
+-
+--- | Read inline, quasiquoted Roy.
+-roy :: QuasiQuoter
+-roy = QuasiQuoter { quoteExp = \s -> do
+-    rs <- roySettings
+-    quoteExp (shakespeare rs) s
+-    }
+-
+--- | Read in a Roy template file. This function reads the file once, at
+--- compile time.
+-royFile :: FilePath -> Q Exp
+-royFile fp = do
+-    rs <- roySettings
+-    shakespeareFile rs fp
+-
+--- | Read in a Roy template file. This impure function uses
+--- unsafePerformIO to re-read the file on every call, allowing for rapid
+--- iteration.
+-royFileReload :: FilePath -> Q Exp
+-royFileReload fp = do
+-    rs <- roySettings
+-    shakespeareFileReload rs fp
+diff --git a/Text/TypeScript.hs b/Text/TypeScript.hs
+index 34bf4bf..30c5388 100644
+--- a/Text/TypeScript.hs
++++ b/Text/TypeScript.hs
+@@ -53,65 +53,10 @@
+ --
+ -- 2. TypeScript: <http://typescript.codeplex.com/>
+ module Text.TypeScript
+-    ( -- * Functions
+-      -- ** Template-Reading Functions
+-      -- | These QuasiQuoter and Template Haskell methods return values of
+-      -- type @'JavascriptUrl' url@. See the Yesod book for details.
+-      tsc
+-    , typeScriptFile
+-    , typeScriptFileReload
+-
+-#ifdef TEST_EXPORT
+-    , typeScriptSettings
+-#endif
++    (
+     ) where
+ 
+ import Language.Haskell.TH.Quote (QuasiQuoter (..))
+ import Language.Haskell.TH.Syntax
+ import Text.Shakespeare
+ import Text.Julius
+-
+--- | The TypeScript language compiles down to Javascript.
+--- We do this compilation once at compile time to avoid needing to do it during the request.
+--- We call this a preConversion because other shakespeare modules like Lucius use Haskell to compile during the request instead rather than a system call.
+-typeScriptSettings :: Q ShakespeareSettings
+-typeScriptSettings = do
+-  jsettings <- javascriptSettings
+-  return $ jsettings { varChar = '#'
+-  , preConversion = Just PreConvert {
+-      preConvert = ReadProcess "sh" ["-c", "TMP_IN=$(mktemp XXXXXXXXXX.ts); TMP_OUT=$(mktemp XXXXXXXXXX.js); cat /dev/stdin > ${TMP_IN} && tsc --out ${TMP_OUT} ${TMP_IN} && cat ${TMP_OUT}; rm ${TMP_IN} && rm ${TMP_OUT}"]
+-    , preEscapeIgnoreBalanced = "'\""
+-    , preEscapeIgnoreLine = "//"
+-    , wrapInsertion = Just WrapInsertion { 
+-        wrapInsertionIndent = Nothing
+-      , wrapInsertionStartBegin = ";(function("
+-      , wrapInsertionSeparator = ", "
+-      , wrapInsertionStartClose = "){"
+-      , wrapInsertionEnd = "})"
+-      , wrapInsertionApplyBegin = "("
+-      , wrapInsertionApplyClose = ");\n"
+-      }
+-    }
+-  }
+-
+--- | Read inline, quasiquoted TypeScript
+-tsc :: QuasiQuoter
+-tsc = QuasiQuoter { quoteExp = \s -> do
+-    rs <- typeScriptSettings
+-    quoteExp (shakespeare rs) s
+-    }
+-
+--- | Read in a Roy template file. This function reads the file once, at
+--- compile time.
+-typeScriptFile :: FilePath -> Q Exp
+-typeScriptFile fp = do
+-    rs <- typeScriptSettings
+-    shakespeareFile rs fp
+-
+--- | Read in a Roy template file. This impure function uses
+--- unsafePerformIO to re-read the file on every call, allowing for rapid
+--- iteration.
+-typeScriptFileReload :: FilePath -> Q Exp
+-typeScriptFileReload fp = do
+-    rs <- typeScriptSettings
+-    shakespeareFileReload rs fp
+-- 
+1.7.10.4
+
diff --git a/standalone/android/haskell-patches/socks-0.4.2_0001-remove-IPv6-stuff.patch b/standalone/android/haskell-patches/socks-0.4.2_0001-remove-IPv6-stuff.patch
new file mode 100644
--- /dev/null
+++ b/standalone/android/haskell-patches/socks-0.4.2_0001-remove-IPv6-stuff.patch
@@ -0,0 +1,107 @@
+From abab0f8202998a3e88c5dc5f67a8245da6c174b3 Mon Sep 17 00:00:00 2001
+From: Joey Hess <joey@kitenet.net>
+Date: Thu, 28 Feb 2013 23:36:20 -0400
+Subject: [PATCH] remove IPv6 stuff
+
+---
+ Network/Socks5.hs         |    1 -
+ Network/Socks5/Command.hs |   16 ++--------------
+ Network/Socks5/Types.hs   |    3 +--
+ Network/Socks5/Wire.hs    |    2 --
+ 4 files changed, 3 insertions(+), 19 deletions(-)
+
+diff --git a/Network/Socks5.hs b/Network/Socks5.hs
+index 67b0060..80efb9c 100644
+--- a/Network/Socks5.hs
++++ b/Network/Socks5.hs
+@@ -54,7 +54,6 @@ socksConnectAddr :: Socket -> SockAddr -> SockAddr -> IO ()
+ socksConnectAddr sock sockserver destaddr = withSocks sock sockserver $ do
+ 	case destaddr of
+ 		SockAddrInet p h      -> socks5ConnectIPV4 sock h p >> return ()
+-		SockAddrInet6 p _ h _ -> socks5ConnectIPV6 sock h p >> return ()
+ 		_                     -> error "unsupported unix sockaddr type"
+ 
+ -- | connect a new socket to the socks server, and connect the stream to a FQDN
+diff --git a/Network/Socks5/Command.hs b/Network/Socks5/Command.hs
+index 2952706..db994c9 100644
+--- a/Network/Socks5/Command.hs
++++ b/Network/Socks5/Command.hs
+@@ -9,9 +9,8 @@
+ --
+ module Network.Socks5.Command
+ 	( socks5Establish
+-	, socks5ConnectIPV4
+-	, socks5ConnectIPV6
+ 	, socks5ConnectDomainName
++	, socks5ConnectIPV4 
+ 	-- * lowlevel interface
+ 	, socks5Rpc
+ 	) where
+@@ -23,7 +22,7 @@ import qualified Data.ByteString as B
+ import qualified Data.ByteString.Char8 as BC
+ import Data.Serialize
+ 
+-import Network.Socket (Socket, PortNumber, HostAddress, HostAddress6)
++import Network.Socket (Socket, PortNumber, HostAddress)
+ import Network.Socket.ByteString
+ 
+ import Network.Socks5.Types
+@@ -46,17 +45,6 @@ socks5ConnectIPV4 socket hostaddr port = onReply <$> socks5Rpc socket request
+ 		onReply (SocksAddrIPV4 h, p) = (h, p)
+ 		onReply _                    = error "ipv4 requested, got something different"
+ 
+-socks5ConnectIPV6 :: Socket -> HostAddress6 -> PortNumber -> IO (HostAddress6, PortNumber)
+-socks5ConnectIPV6 socket hostaddr6 port = onReply <$> socks5Rpc socket request
+-	where
+-		request = SocksRequest
+-			{ requestCommand  = SocksCommandConnect
+-			, requestDstAddr  = SocksAddrIPV6 hostaddr6
+-			, requestDstPort  = fromIntegral port
+-			}
+-		onReply (SocksAddrIPV6 h, p) = (h, p)
+-		onReply _                    = error "ipv6 requested, got something different"
+-
+ -- TODO: FQDN should only be ascii, maybe putting a "fqdn" data type
+ -- in front to make sure and make the BC.pack safe.
+ socks5ConnectDomainName :: Socket -> String -> PortNumber -> IO (SocksAddr, PortNumber)
+diff --git a/Network/Socks5/Types.hs b/Network/Socks5/Types.hs
+index 5dc7d5e..12dea99 100644
+--- a/Network/Socks5/Types.hs
++++ b/Network/Socks5/Types.hs
+@@ -17,7 +17,7 @@ module Network.Socks5.Types
+ import Data.ByteString (ByteString)
+ import Data.Word
+ import Data.Data
+-import Network.Socket (HostAddress, HostAddress6)
++import Network.Socket (HostAddress)
+ import Control.Exception
+ 
+ data SocksCommand =
+@@ -38,7 +38,6 @@ data SocksMethod =
+ data SocksAddr =
+ 	  SocksAddrIPV4 HostAddress
+ 	| SocksAddrDomainName ByteString
+-	| SocksAddrIPV6 HostAddress6
+ 	deriving (Show,Eq)
+ 
+ data SocksReply =
+diff --git a/Network/Socks5/Wire.hs b/Network/Socks5/Wire.hs
+index 2cfed52..d3bd9c5 100644
+--- a/Network/Socks5/Wire.hs
++++ b/Network/Socks5/Wire.hs
+@@ -41,12 +41,10 @@ data SocksResponse = SocksResponse
+ 
+ getAddr 1 = SocksAddrIPV4 <$> getWord32be
+ getAddr 3 = SocksAddrDomainName <$> (getWord8 >>= getByteString . fromIntegral)
+-getAddr 4 = SocksAddrIPV6 <$> (liftM4 (,,,) getWord32le getWord32le getWord32le getWord32le)
+ getAddr n = error ("cannot get unknown socket address type: " ++ show n)
+ 
+ putAddr (SocksAddrIPV4 h)         = putWord8 1 >> putWord32host h
+ putAddr (SocksAddrDomainName b)   = putWord8 3 >> putWord8 (fromIntegral $ B.length b) >> putByteString b
+-putAddr (SocksAddrIPV6 (a,b,c,d)) = putWord8 4 >> mapM_ putWord32host [a,b,c,d]
+ 
+ getSocksRequest 5 = do
+ 	cmd <- toEnum . fromIntegral <$> getWord8
+-- 
+1.7.10.4
+
diff --git a/standalone/android/haskell-patches/yesod-core-1.1.8_0001-remove-TH.patch b/standalone/android/haskell-patches/yesod-core-1.1.8_0001-remove-TH.patch
new file mode 100644
--- /dev/null
+++ b/standalone/android/haskell-patches/yesod-core-1.1.8_0001-remove-TH.patch
@@ -0,0 +1,476 @@
+From 801f6dea3be43113400e41aabb443456fffcd227 Mon Sep 17 00:00:00 2001
+From: Joey Hess <joey@kitenet.net>
+Date: Thu, 28 Feb 2013 23:39:40 -0400
+Subject: [PATCH 1/2] remove TH
+
+---
+ Yesod/Core.hs           |   10 ----
+ Yesod/Dispatch.hs       |  119 +----------------------------------------------
+ Yesod/Handler.hs        |   27 +----------
+ Yesod/Internal/Cache.hs |    5 --
+ Yesod/Internal/Core.hs  |  119 +++++------------------------------------------
+ Yesod/Widget.hs         |   29 ------------
+ 6 files changed, 13 insertions(+), 296 deletions(-)
+
+diff --git a/Yesod/Core.hs b/Yesod/Core.hs
+index 7268d6c..ce04b7d 100644
+--- a/Yesod/Core.hs
++++ b/Yesod/Core.hs
+@@ -21,16 +21,6 @@ module Yesod.Core
+     , unauthorizedI
+       -- * Logging
+     , LogLevel (..)
+-    , logDebug
+-    , logInfo
+-    , logWarn
+-    , logError
+-    , logOther
+-    , logDebugS
+-    , logInfoS
+-    , logWarnS
+-    , logErrorS
+-    , logOtherS
+       -- * Sessions
+     , SessionBackend (..)
+     , defaultClientSessionBackend
+diff --git a/Yesod/Dispatch.hs b/Yesod/Dispatch.hs
+index 1e19388..dd37475 100644
+--- a/Yesod/Dispatch.hs
++++ b/Yesod/Dispatch.hs
+@@ -6,20 +6,9 @@
+ {-# LANGUAGE MultiParamTypeClasses #-}
+ module Yesod.Dispatch
+     ( -- * Quasi-quoted routing
+-      parseRoutes
+-    , parseRoutesNoCheck
+-    , parseRoutesFile
+-    , parseRoutesFileNoCheck
+-    , mkYesod
+-    , mkYesodSub
+       -- ** More fine-grained
+-    , mkYesodData
+-    , mkYesodSubData
+-    , mkYesodDispatch
+-    , mkYesodSubDispatch
+-    , mkDispatchInstance
+       -- ** Path pieces
+-    , PathPiece (..)
++      PathPiece (..)
+     , PathMultiPiece (..)
+     , Texts
+       -- * Convert to WAI
+@@ -52,117 +41,11 @@ import Data.Monoid (mappend)
+ import qualified Data.ByteString as S
+ import qualified Blaze.ByteString.Builder
+ import Network.HTTP.Types (status301)
+-import Yesod.Routes.TH
+ import Yesod.Content (chooseRep)
+-import Yesod.Routes.Parse
+ import System.Log.FastLogger (Logger)
+ 
+ type Texts = [Text]
+ 
+--- | Generates URL datatype and site function for the given 'Resource's. This
+--- is used for creating sites, /not/ subsites. See 'mkYesodSub' for the latter.
+--- Use 'parseRoutes' to create the 'Resource's.
+-mkYesod :: String -- ^ name of the argument datatype
+-        -> [ResourceTree String]
+-        -> Q [Dec]
+-mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] [] False
+-
+--- | Generates URL datatype and site function for the given 'Resource's. This
+--- is used for creating subsites, /not/ sites. See 'mkYesod' for the latter.
+--- Use 'parseRoutes' to create the 'Resource's. In general, a subsite is not
+--- executable by itself, but instead provides functionality to
+--- be embedded in other sites.
+-mkYesodSub :: String -- ^ name of the argument datatype
+-           -> Cxt
+-           -> [ResourceTree String]
+-           -> Q [Dec]
+-mkYesodSub name clazzes =
+-    fmap (uncurry (++)) . mkYesodGeneral name' rest clazzes True
+-  where
+-    (name':rest) = words name
+-
+--- | Sometimes, you will want to declare your routes in one file and define
+--- your handlers elsewhere. For example, this is the only way to break up a
+--- monolithic file into smaller parts. Use this function, paired with
+--- 'mkYesodDispatch', to do just that.
+-mkYesodData :: String -> [ResourceTree String] -> Q [Dec]
+-mkYesodData name res = mkYesodDataGeneral name [] False res
+-
+-mkYesodSubData :: String -> Cxt -> [ResourceTree String] -> Q [Dec]
+-mkYesodSubData name clazzes res = mkYesodDataGeneral name clazzes True res
+-
+-mkYesodDataGeneral :: String -> Cxt -> Bool -> [ResourceTree String] -> Q [Dec]
+-mkYesodDataGeneral name clazzes isSub res = do
+-    let (name':rest) = words name
+-    (x, _) <- mkYesodGeneral name' rest clazzes isSub res
+-    let rname = mkName $ "resources" ++ name
+-    eres <- lift res
+-    let y = [ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)
+-            , FunD rname [Clause [] (NormalB eres) []]
+-            ]
+-    return $ x ++ y
+-
+--- | See 'mkYesodData'.
+-mkYesodDispatch :: String -> [ResourceTree String] -> Q [Dec]
+-mkYesodDispatch name = fmap snd . mkYesodGeneral name [] [] False
+-
+-mkYesodSubDispatch :: String -> Cxt -> [ResourceTree String] -> Q [Dec]
+-mkYesodSubDispatch name clazzes = fmap snd . mkYesodGeneral name' rest clazzes True 
+-  where (name':rest) = words name
+-
+-mkYesodGeneral :: String                   -- ^ foundation type
+-               -> [String]                 -- ^ arguments for the type
+-               -> Cxt                      -- ^ the type constraints
+-               -> Bool                     -- ^ it this a subsite
+-               -> [ResourceTree String]
+-               -> Q([Dec],[Dec])
+-mkYesodGeneral name args clazzes isSub resS = do
+-     subsite        <- sub
+-     masterTypeSyns <- if isSub then return   [] 
+-                                else sequence [handler, widget]
+-     renderRouteDec <- mkRenderRouteInstance subsite res
+-     dispatchDec    <- mkDispatchInstance context sub master res
+-     return (renderRouteDec ++ masterTypeSyns, dispatchDec)
+-  where sub     = foldl appT subCons subArgs
+-        master  = if isSub then (varT $ mkName "master") else sub
+-        context = if isSub then cxt $ yesod : map return clazzes
+-                           else return []
+-        yesod   = classP ''Yesod [master]
+-        handler = tySynD (mkName "Handler") [] [t| GHandler $master $master    |]
+-        widget  = tySynD (mkName "Widget")  [] [t| GWidget  $master $master () |]
+-        res     = map (fmap parseType) resS
+-        subCons = conT $ mkName name
+-        subArgs = map (varT. mkName) args
+-        
+--- | If the generation of @'YesodDispatch'@ instance require finer
+--- control of the types, contexts etc. using this combinator. You will
+--- hardly need this generality. However, in certain situations, like
+--- when writing library/plugin for yesod, this combinator becomes
+--- handy.
+-mkDispatchInstance :: CxtQ                -- ^ The context
+-                   -> TypeQ               -- ^ The subsite type
+-                   -> TypeQ               -- ^ The master site type
+-                   -> [ResourceTree a]    -- ^ The resource
+-                   -> DecsQ
+-mkDispatchInstance context sub master res = do
+-  logger <- newName "logger"
+-  let loggerE = varE logger
+-      loggerP = VarP logger
+-      yDispatch = conT ''YesodDispatch `appT` sub `appT` master
+-      thisDispatch = do
+-            Clause pat body decs <- mkDispatchClause
+-                                    [|yesodRunner   $loggerE |]
+-                                    [|yesodDispatch $loggerE |]
+-                                    [|fmap chooseRep|]
+-                                    res
+-            return $ FunD 'yesodDispatch
+-                         [ Clause (loggerP:pat)
+-                                  body
+-                                  decs
+-                         ]
+-      in sequence [instanceD context yDispatch [thisDispatch]]
+-        
+-
+ -- | Convert the given argument into a WAI application, executable with any WAI
+ -- handler. This is the same as 'toWaiAppPlain', except it includes two
+ -- middlewares: GZIP compression and autohead. This is the
+diff --git a/Yesod/Handler.hs b/Yesod/Handler.hs
+index 1997bdb..98c915c 100644
+--- a/Yesod/Handler.hs
++++ b/Yesod/Handler.hs
+@@ -42,7 +42,6 @@ module Yesod.Handler
+     , RedirectUrl (..)
+     , redirect
+     , redirectWith
+-    , redirectToPost
+       -- ** Errors
+     , notFound
+     , badMethod
+@@ -100,7 +99,6 @@ module Yesod.Handler
+     , getMessageRender
+       -- * Per-request caching
+     , CacheKey
+-    , mkCacheKey
+     , cacheLookup
+     , cacheInsert
+     , cacheDelete
+@@ -172,7 +170,7 @@ import System.Log.FastLogger
+ import Control.Monad.Logger
+ 
+ import qualified Yesod.Internal.Cache as Cache
+-import Yesod.Internal.Cache (mkCacheKey, CacheKey)
++import Yesod.Internal.Cache (CacheKey)
+ import qualified Data.IORef as I
+ import Control.Exception.Lifted (catch)
+ import Control.Monad.Trans.Control
+@@ -937,29 +935,6 @@ newIdent = do
+     put x { ghsIdent = i' }
+     return $ T.pack $ 'h' : show i'
+ 
+--- | Redirect to a POST resource.
+---
+--- This is not technically a redirect; instead, it returns an HTML page with a
+--- POST form, and some Javascript to automatically submit the form. This can be
+--- useful when you need to post a plain link somewhere that needs to cause
+--- changes on the server.
+-redirectToPost :: RedirectUrl master url => url -> GHandler sub master a
+-redirectToPost url = do
+-    urlText <- toTextUrl url
+-    hamletToRepHtml [hamlet|
+-$newline never
+-$doctype 5
+-
+-<html>
+-    <head>
+-        <title>Redirecting...
+-    <body onload="document.getElementById('form').submit()">
+-        <form id="form" method="post" action=#{urlText}>
+-            <noscript>
+-                <p>Javascript has been disabled; please click on the button below to be redirected.
+-            <input type="submit" value="Continue">
+-|] >>= sendResponse
+-
+ -- | Converts the given Hamlet template into 'Content', which can be used in a
+ -- Yesod 'Response'.
+ hamletToContent :: HtmlUrl (Route master) -> GHandler sub master Content
+diff --git a/Yesod/Internal/Cache.hs b/Yesod/Internal/Cache.hs
+index 4aec0d2..fdef9d7 100644
+--- a/Yesod/Internal/Cache.hs
++++ b/Yesod/Internal/Cache.hs
+@@ -3,7 +3,6 @@
+ module Yesod.Internal.Cache
+     ( Cache
+     , CacheKey
+-    , mkCacheKey
+     , lookup
+     , insert
+     , delete
+@@ -24,10 +23,6 @@ newtype Cache = Cache (Map.IntMap Any)
+ 
+ newtype CacheKey a = CacheKey Int
+ 
+--- | Generate a new 'CacheKey'. Be sure to give a full type signature.
+-mkCacheKey :: Q Exp
+-mkCacheKey = [|CacheKey|] `appE` (LitE . IntegerL . fromIntegral . hashUnique <$> runIO newUnique)
+-
+ lookup :: CacheKey a -> Cache -> Maybe a
+ lookup (CacheKey i) (Cache m) = unsafeCoerce <$> Map.lookup i m
+ 
+diff --git a/Yesod/Internal/Core.hs b/Yesod/Internal/Core.hs
+index c4a9796..90c05fc 100644
+--- a/Yesod/Internal/Core.hs
++++ b/Yesod/Internal/Core.hs
+@@ -44,7 +44,6 @@ module Yesod.Internal.Core
+ 
+ import Yesod.Content
+ import Yesod.Handler hiding (lift, getExpires)
+-import Control.Monad.Logger (logErrorS)
+ 
+ import Yesod.Routes.Class
+ import Data.Time (UTCTime, addUTCTime, getCurrentTime)
+@@ -165,22 +164,7 @@ class RenderRoute a => Yesod a where
+ 
+     -- | Applies some form of layout to the contents of a page.
+     defaultLayout :: GWidget sub a () -> GHandler sub a RepHtml
+-    defaultLayout w = do
+-        p <- widgetToPageContent w
+-        mmsg <- getMessage
+-        hamletToRepHtml [hamlet|
+-$newline never
+-$doctype 5
+-
+-<html>
+-    <head>
+-        <title>#{pageTitle p}
+-        ^{pageHead p}
+-    <body>
+-        $maybe msg <- mmsg
+-            <p .message>#{msg}
+-        ^{pageBody p}
+-|]
++    defaultLayout w = error "defaultLayout not implemented"
+ 
+     -- | Override the rendering function for a particular URL. One use case for
+     -- this is to offload static hosting to a different domain name to avoid
+@@ -521,46 +505,11 @@ applyLayout' title body = fmap chooseRep $ defaultLayout $ do
+ 
+ -- | The default error handler for 'errorHandler'.
+ defaultErrorHandler :: Yesod y => ErrorResponse -> GHandler sub y ChooseRep
+-defaultErrorHandler NotFound = do
+-    r <- waiRequest
+-    let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r
+-    applyLayout' "Not Found"
+-        [hamlet|
+-$newline never
+-<h1>Not Found
+-<p>#{path'}
+-|]
+-defaultErrorHandler (PermissionDenied msg) =
+-    applyLayout' "Permission Denied"
+-        [hamlet|
+-$newline never
+-<h1>Permission denied
+-<p>#{msg}
+-|]
+-defaultErrorHandler (InvalidArgs ia) =
+-    applyLayout' "Invalid Arguments"
+-        [hamlet|
+-$newline never
+-<h1>Invalid Arguments
+-<ul>
+-    $forall msg <- ia
+-        <li>#{msg}
+-|]
+-defaultErrorHandler (InternalError e) = do
+-    $logErrorS "yesod-core" e
+-    applyLayout' "Internal Server Error"
+-        [hamlet|
+-$newline never
+-<h1>Internal Server Error
+-<pre>#{e}
+-|]
+-defaultErrorHandler (BadMethod m) =
+-    applyLayout' "Bad Method"
+-        [hamlet|
+-$newline never
+-<h1>Method Not Supported
+-<p>Method <code>#{S8.unpack m}</code> not supported
+-|]
++defaultErrorHandler NotFound = error "Not Found"
++defaultErrorHandler (PermissionDenied msg) = error "Permission Denied"
++defaultErrorHandler (InvalidArgs ia) = error "Invalid Arguments"
++defaultErrorHandler (InternalError e) = error "Internal Server Error"
++defaultErrorHandler (BadMethod m) = error "Bad Method"
+ 
+ -- | Return the same URL if the user is authorized to see it.
+ --
+@@ -616,45 +565,10 @@ widgetToPageContent w = do
+     -- modernizr should be at the end of the <head> http://www.modernizr.com/docs/#installing
+     -- the asynchronous loader means your page doesn't have to wait for all the js to load
+     let (mcomplete, asyncScripts) = asyncHelper render scripts jscript jsLoc
+-        regularScriptLoad = [hamlet|
+-$newline never
+-$forall s <- scripts
+-    ^{mkScriptTag s}
+-$maybe j <- jscript
+-    $maybe s <- jsLoc
+-        <script src="#{s}">
+-    $nothing
+-        <script>^{jelper j}
+-|]
+-
+-        headAll = [hamlet|
+-$newline never
+-\^{head'}
+-$forall s <- stylesheets
+-    ^{mkLinkTag s}
+-$forall s <- css
+-    $maybe t <- right $ snd s
+-        $maybe media <- fst s
+-            <link rel=stylesheet media=#{media} href=#{t}>
+-        $nothing
+-            <link rel=stylesheet href=#{t}>
+-    $maybe content <- left $ snd s
+-        $maybe media <- fst s
+-            <style media=#{media}>#{content}
+-        $nothing
+-            <style>#{content}
+-$case jsLoader master
+-  $of BottomOfBody
+-  $of BottomOfHeadAsync asyncJsLoader
+-      ^{asyncJsLoader asyncScripts mcomplete}
+-  $of BottomOfHeadBlocking
+-      ^{regularScriptLoad}
+-|]
+-    let bodyScript = [hamlet|
+-$newline never
+-^{body}
+-^{regularScriptLoad}
+-|]
++        regularScriptLoad = error "TODO"
++
++        headAll = error "TODO"
++    let bodyScript = error "TODO"
+ 
+     return $ PageContent title headAll (case jsLoader master of
+       BottomOfBody -> bodyScript
+@@ -696,18 +610,7 @@ jsonArray = unsafeLazyByteString . encode . Array . Vector.fromList . map String
+ 
+ -- | For use with setting 'jsLoader' to 'BottomOfHeadAsync'
+ loadJsYepnope :: Yesod master => Either Text (Route master) -> [Text] -> Maybe (HtmlUrl (Route master)) -> (HtmlUrl (Route master))
+-loadJsYepnope eyn scripts mcomplete =
+-  [hamlet|
+-$newline never
+-    $maybe yn <- left eyn
+-        <script src=#{yn}>
+-    $maybe yn <- right eyn
+-        <script src=@{yn}>
+-    $maybe complete <- mcomplete
+-        <script>yepnope({load:#{jsonArray scripts},complete:function(){^{complete}}});
+-    $nothing
+-        <script>yepnope({load:#{jsonArray scripts}});
+-|]
++loadJsYepnope eyn scripts mcomplete = error "TODO"
+ 
+ asyncHelper :: (url -> [x] -> Text)
+          -> [Script (url)]
+diff --git a/Yesod/Widget.hs b/Yesod/Widget.hs
+index bd94bd3..bf79150 100644
+--- a/Yesod/Widget.hs
++++ b/Yesod/Widget.hs
+@@ -15,8 +15,6 @@ module Yesod.Widget
+       GWidget
+     , PageContent (..)
+       -- * Special Hamlet quasiquoter/TH for Widgets
+-    , whamlet
+-    , whamletFile
+     , ihamletToRepHtml
+       -- * Convert to Widget
+     , ToWidget (..)
+@@ -54,7 +52,6 @@ module Yesod.Widget
+     , addScriptEither
+       -- * Internal
+     , unGWidget
+-    , whamletFileWithSettings
+     ) where
+ 
+ import Data.Monoid
+@@ -274,32 +271,6 @@ data PageContent url = PageContent
+     , pageBody :: HtmlUrl url
+     }
+ 
+-whamlet :: QuasiQuoter
+-whamlet = NP.hamletWithSettings rules NP.defaultHamletSettings
+-
+-whamletFile :: FilePath -> Q Exp
+-whamletFile = NP.hamletFileWithSettings rules NP.defaultHamletSettings
+-
+-whamletFileWithSettings :: NP.HamletSettings -> FilePath -> Q Exp
+-whamletFileWithSettings = NP.hamletFileWithSettings rules
+-
+-rules :: Q NP.HamletRules
+-rules = do
+-    ah <- [|toWidget|]
+-    let helper qg f = do
+-            x <- newName "urender"
+-            e <- f $ VarE x
+-            let e' = LamE [VarP x] e
+-            g <- qg
+-            bind <- [|(>>=)|]
+-            return $ InfixE (Just g) bind (Just e')
+-    let ur f = do
+-            let env = NP.Env
+-                    (Just $ helper [|liftW getUrlRenderParams|])
+-                    (Just $ helper [|liftM (toHtml .) $ liftW getMessageRender|])
+-            f env
+-    return $ NP.HamletRules ah ur $ \_ b -> return $ ah `AppE` b
+-
+ -- | Wraps the 'Content' generated by 'hamletToContent' in a 'RepHtml'.
+ ihamletToRepHtml :: RenderMessage master message
+                  => HtmlUrlI18n message (Route master)
+-- 
+1.7.10.4
+
diff --git a/standalone/android/runshell b/standalone/android/runshell
--- a/standalone/android/runshell
+++ b/standalone/android/runshell
@@ -25,7 +25,7 @@
 	for prog in busybox git-annex git-shell git-upload-pack git gpg rsync ssh ssh-keygen; do
 		$cmd echo "installing $prog"
 		if $cmd test -e "$base/bin/$prog"; then
-			$cmd rm "$base/bin/$prog"
+			$cmd rm -f "$base/bin/$prog"
 		fi
 		$cmd ln "$base/lib/lib.$prog.so" "$base/bin/$prog"
 	done
@@ -39,11 +39,11 @@
 		for link in $($cmd cat "$base/links/$prog"); do
 			$cmd echo "linking $link to $prog"
 			if $cmd test -e "$base/$link"; then
-				$cmd rm "$base/$link"
+				$cmd rm -f "$base/$link"
 			fi
 			$cmd ln "$base/bin/$prog" "$base/$link"
 		done
-		$cmd rm "$base/links/$prog"
+		$cmd rm -f "$base/links/$prog"
 	done
 
 	$cmd mkdir -p "$base/templates"
@@ -54,7 +54,7 @@
 }
 
 install () {
-	if $cmd test ! -e "$base/git-annex"; then
+	if $cmd test ! -e "$base/bin/git-annex"; then
 		if ! $cmd mkdir -p "$HOME"; then
 			$cmd echo "mkdir of $HOME failed!"
 		fi
@@ -100,7 +100,7 @@
 		shift 1
 		exec "$cmd" "$@"
 	else
-		if $cmd test -e "$HOME/.config/git-annex/autostart; then
+		if $cmd test -e "$HOME/.config/git-annex/autostart"; then
 			git annex assistant --autostart || $cmd true
 		fi
 		/system/bin/sh
diff --git a/standalone/android/start.c b/standalone/android/start.c
--- a/standalone/android/start.c
+++ b/standalone/android/start.c
@@ -38,6 +38,15 @@
 		exit(1);
 	}
 
+	if (stat("lib/lib.busybox.so", &st_buf) != 0) {
+		/* TODO my lib dir should be in LD_LIBRARY_PATH; check that */
+		fprintf(stderr, "Falling back to hardcoded app location; cannot find expected files in %s\n", buf);
+		if (chdir("/data/data/ga.androidterm") != 0) {
+			perror("chdir");
+			exit(1);
+		}
+	}
+
 	/* If this is the first run, set up busybox link. */
 	if (stat("busybox", &st_buf) != 0) {
 		if (link("lib/lib.busybox.so", "busybox") != 0) {
diff --git a/static/jquery.ui.core.js b/static/jquery.ui.core.js
new file mode 100644
--- /dev/null
+++ b/static/jquery.ui.core.js
@@ -0,0 +1,324 @@
+/*!
+ * jQuery UI Core 1.10.1
+ * http://jqueryui.com
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/category/ui-core/
+ */
+(function( $, undefined ) {
+
+var uuid = 0,
+	runiqueId = /^ui-id-\d+$/;
+
+// prevent duplicate loading
+// this is only a problem because we proxy existing functions
+// and we don't want to double proxy them
+$.ui = $.ui || {};
+if ( $.ui.version ) {
+	return;
+}
+
+$.extend( $.ui, {
+	version: "1.10.1",
+
+	keyCode: {
+		BACKSPACE: 8,
+		COMMA: 188,
+		DELETE: 46,
+		DOWN: 40,
+		END: 35,
+		ENTER: 13,
+		ESCAPE: 27,
+		HOME: 36,
+		LEFT: 37,
+		NUMPAD_ADD: 107,
+		NUMPAD_DECIMAL: 110,
+		NUMPAD_DIVIDE: 111,
+		NUMPAD_ENTER: 108,
+		NUMPAD_MULTIPLY: 106,
+		NUMPAD_SUBTRACT: 109,
+		PAGE_DOWN: 34,
+		PAGE_UP: 33,
+		PERIOD: 190,
+		RIGHT: 39,
+		SPACE: 32,
+		TAB: 9,
+		UP: 38
+	}
+});
+
+// plugins
+$.fn.extend({
+	_focus: $.fn.focus,
+	focus: function( delay, fn ) {
+		return typeof delay === "number" ?
+			this.each(function() {
+				var elem = this;
+				setTimeout(function() {
+					$( elem ).focus();
+					if ( fn ) {
+						fn.call( elem );
+					}
+				}, delay );
+			}) :
+			this._focus.apply( this, arguments );
+	},
+
+	scrollParent: function() {
+		var scrollParent;
+		if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) {
+			scrollParent = this.parents().filter(function() {
+				return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x"));
+			}).eq(0);
+		} else {
+			scrollParent = this.parents().filter(function() {
+				return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x"));
+			}).eq(0);
+		}
+
+		return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent;
+	},
+
+	zIndex: function( zIndex ) {
+		if ( zIndex !== undefined ) {
+			return this.css( "zIndex", zIndex );
+		}
+
+		if ( this.length ) {
+			var elem = $( this[ 0 ] ), position, value;
+			while ( elem.length && elem[ 0 ] !== document ) {
+				// Ignore z-index if position is set to a value where z-index is ignored by the browser
+				// This makes behavior of this function consistent across browsers
+				// WebKit always returns auto if the element is positioned
+				position = elem.css( "position" );
+				if ( position === "absolute" || position === "relative" || position === "fixed" ) {
+					// IE returns 0 when zIndex is not specified
+					// other browsers return a string
+					// we ignore the case of nested elements with an explicit value of 0
+					// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
+					value = parseInt( elem.css( "zIndex" ), 10 );
+					if ( !isNaN( value ) && value !== 0 ) {
+						return value;
+					}
+				}
+				elem = elem.parent();
+			}
+		}
+
+		return 0;
+	},
+
+	uniqueId: function() {
+		return this.each(function() {
+			if ( !this.id ) {
+				this.id = "ui-id-" + (++uuid);
+			}
+		});
+	},
+
+	removeUniqueId: function() {
+		return this.each(function() {
+			if ( runiqueId.test( this.id ) ) {
+				$( this ).removeAttr( "id" );
+			}
+		});
+	}
+});
+
+// selectors
+function focusable( element, isTabIndexNotNaN ) {
+	var map, mapName, img,
+		nodeName = element.nodeName.toLowerCase();
+	if ( "area" === nodeName ) {
+		map = element.parentNode;
+		mapName = map.name;
+		if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
+			return false;
+		}
+		img = $( "img[usemap=#" + mapName + "]" )[0];
+		return !!img && visible( img );
+	}
+	return ( /input|select|textarea|button|object/.test( nodeName ) ?
+		!element.disabled :
+		"a" === nodeName ?
+			element.href || isTabIndexNotNaN :
+			isTabIndexNotNaN) &&
+		// the element and all of its ancestors must be visible
+		visible( element );
+}
+
+function visible( element ) {
+	return $.expr.filters.visible( element ) &&
+		!$( element ).parents().addBack().filter(function() {
+			return $.css( this, "visibility" ) === "hidden";
+		}).length;
+}
+
+$.extend( $.expr[ ":" ], {
+	data: $.expr.createPseudo ?
+		$.expr.createPseudo(function( dataName ) {
+			return function( elem ) {
+				return !!$.data( elem, dataName );
+			};
+		}) :
+		// support: jQuery <1.8
+		function( elem, i, match ) {
+			return !!$.data( elem, match[ 3 ] );
+		},
+
+	focusable: function( element ) {
+		return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) );
+	},
+
+	tabbable: function( element ) {
+		var tabIndex = $.attr( element, "tabindex" ),
+			isTabIndexNaN = isNaN( tabIndex );
+		return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );
+	}
+});
+
+// support: jQuery <1.8
+if ( !$( "<a>" ).outerWidth( 1 ).jquery ) {
+	$.each( [ "Width", "Height" ], function( i, name ) {
+		var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
+			type = name.toLowerCase(),
+			orig = {
+				innerWidth: $.fn.innerWidth,
+				innerHeight: $.fn.innerHeight,
+				outerWidth: $.fn.outerWidth,
+				outerHeight: $.fn.outerHeight
+			};
+
+		function reduce( elem, size, border, margin ) {
+			$.each( side, function() {
+				size -= parseFloat( $.css( elem, "padding" + this ) ) || 0;
+				if ( border ) {
+					size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0;
+				}
+				if ( margin ) {
+					size -= parseFloat( $.css( elem, "margin" + this ) ) || 0;
+				}
+			});
+			return size;
+		}
+
+		$.fn[ "inner" + name ] = function( size ) {
+			if ( size === undefined ) {
+				return orig[ "inner" + name ].call( this );
+			}
+
+			return this.each(function() {
+				$( this ).css( type, reduce( this, size ) + "px" );
+			});
+		};
+
+		$.fn[ "outer" + name] = function( size, margin ) {
+			if ( typeof size !== "number" ) {
+				return orig[ "outer" + name ].call( this, size );
+			}
+
+			return this.each(function() {
+				$( this).css( type, reduce( this, size, true, margin ) + "px" );
+			});
+		};
+	});
+}
+
+// support: jQuery <1.8
+if ( !$.fn.addBack ) {
+	$.fn.addBack = function( selector ) {
+		return this.add( selector == null ?
+			this.prevObject : this.prevObject.filter( selector )
+		);
+	};
+}
+
+// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413)
+if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) {
+	$.fn.removeData = (function( removeData ) {
+		return function( key ) {
+			if ( arguments.length ) {
+				return removeData.call( this, $.camelCase( key ) );
+			} else {
+				return removeData.call( this );
+			}
+		};
+	})( $.fn.removeData );
+}
+
+
+
+
+
+// deprecated
+$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() );
+
+$.support.selectstart = "onselectstart" in document.createElement( "div" );
+$.fn.extend({
+	disableSelection: function() {
+		return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) +
+			".ui-disableSelection", function( event ) {
+				event.preventDefault();
+			});
+	},
+
+	enableSelection: function() {
+		return this.unbind( ".ui-disableSelection" );
+	}
+});
+
+$.extend( $.ui, {
+	// $.ui.plugin is deprecated.  Use the proxy pattern instead.
+	plugin: {
+		add: function( module, option, set ) {
+			var i,
+				proto = $.ui[ module ].prototype;
+			for ( i in set ) {
+				proto.plugins[ i ] = proto.plugins[ i ] || [];
+				proto.plugins[ i ].push( [ option, set[ i ] ] );
+			}
+		},
+		call: function( instance, name, args ) {
+			var i,
+				set = instance.plugins[ name ];
+			if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) {
+				return;
+			}
+
+			for ( i = 0; i < set.length; i++ ) {
+				if ( instance.options[ set[ i ][ 0 ] ] ) {
+					set[ i ][ 1 ].apply( instance.element, args );
+				}
+			}
+		}
+	},
+
+	// only used by resizable
+	hasScroll: function( el, a ) {
+
+		//If overflow is hidden, the element might have extra content, but the user wants to hide it
+		if ( $( el ).css( "overflow" ) === "hidden") {
+			return false;
+		}
+
+		var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
+			has = false;
+
+		if ( el[ scroll ] > 0 ) {
+			return true;
+		}
+
+		// TODO: determine which cases actually cause this to happen
+		// if the element doesn't have the scroll set, see if it's possible to
+		// set the scroll
+		el[ scroll ] = 1;
+		has = ( el[ scroll ] > 0 );
+		el[ scroll ] = 0;
+		return has;
+	}
+});
+
+})( jQuery );
diff --git a/static/jquery.ui.mouse.js b/static/jquery.ui.mouse.js
new file mode 100644
--- /dev/null
+++ b/static/jquery.ui.mouse.js
@@ -0,0 +1,169 @@
+/*!
+ * jQuery UI Mouse 1.10.1
+ * http://jqueryui.com
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/mouse/
+ *
+ * Depends:
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+var mouseHandled = false;
+$( document ).mouseup( function() {
+	mouseHandled = false;
+});
+
+$.widget("ui.mouse", {
+	version: "1.10.1",
+	options: {
+		cancel: "input,textarea,button,select,option",
+		distance: 1,
+		delay: 0
+	},
+	_mouseInit: function() {
+		var that = this;
+
+		this.element
+			.bind("mousedown."+this.widgetName, function(event) {
+				return that._mouseDown(event);
+			})
+			.bind("click."+this.widgetName, function(event) {
+				if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) {
+					$.removeData(event.target, that.widgetName + ".preventClickEvent");
+					event.stopImmediatePropagation();
+					return false;
+				}
+			});
+
+		this.started = false;
+	},
+
+	// TODO: make sure destroying one instance of mouse doesn't mess with
+	// other instances of mouse
+	_mouseDestroy: function() {
+		this.element.unbind("."+this.widgetName);
+		if ( this._mouseMoveDelegate ) {
+			$(document)
+				.unbind("mousemove."+this.widgetName, this._mouseMoveDelegate)
+				.unbind("mouseup."+this.widgetName, this._mouseUpDelegate);
+		}
+	},
+
+	_mouseDown: function(event) {
+		// don't let more than one widget handle mouseStart
+		if( mouseHandled ) { return; }
+
+		// we may have missed mouseup (out of window)
+		(this._mouseStarted && this._mouseUp(event));
+
+		this._mouseDownEvent = event;
+
+		var that = this,
+			btnIsLeft = (event.which === 1),
+			// event.target.nodeName works around a bug in IE 8 with
+			// disabled inputs (#7620)
+			elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false);
+		if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) {
+			return true;
+		}
+
+		this.mouseDelayMet = !this.options.delay;
+		if (!this.mouseDelayMet) {
+			this._mouseDelayTimer = setTimeout(function() {
+				that.mouseDelayMet = true;
+			}, this.options.delay);
+		}
+
+		if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
+			this._mouseStarted = (this._mouseStart(event) !== false);
+			if (!this._mouseStarted) {
+				event.preventDefault();
+				return true;
+			}
+		}
+
+		// Click event may never have fired (Gecko & Opera)
+		if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) {
+			$.removeData(event.target, this.widgetName + ".preventClickEvent");
+		}
+
+		// these delegates are required to keep context
+		this._mouseMoveDelegate = function(event) {
+			return that._mouseMove(event);
+		};
+		this._mouseUpDelegate = function(event) {
+			return that._mouseUp(event);
+		};
+		$(document)
+			.bind("mousemove."+this.widgetName, this._mouseMoveDelegate)
+			.bind("mouseup."+this.widgetName, this._mouseUpDelegate);
+
+		event.preventDefault();
+
+		mouseHandled = true;
+		return true;
+	},
+
+	_mouseMove: function(event) {
+		// IE mouseup check - mouseup happened when mouse was out of window
+		if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) {
+			return this._mouseUp(event);
+		}
+
+		if (this._mouseStarted) {
+			this._mouseDrag(event);
+			return event.preventDefault();
+		}
+
+		if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) {
+			this._mouseStarted =
+				(this._mouseStart(this._mouseDownEvent, event) !== false);
+			(this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event));
+		}
+
+		return !this._mouseStarted;
+	},
+
+	_mouseUp: function(event) {
+		$(document)
+			.unbind("mousemove."+this.widgetName, this._mouseMoveDelegate)
+			.unbind("mouseup."+this.widgetName, this._mouseUpDelegate);
+
+		if (this._mouseStarted) {
+			this._mouseStarted = false;
+
+			if (event.target === this._mouseDownEvent.target) {
+				$.data(event.target, this.widgetName + ".preventClickEvent", true);
+			}
+
+			this._mouseStop(event);
+		}
+
+		return false;
+	},
+
+	_mouseDistanceMet: function(event) {
+		return (Math.max(
+				Math.abs(this._mouseDownEvent.pageX - event.pageX),
+				Math.abs(this._mouseDownEvent.pageY - event.pageY)
+			) >= this.options.distance
+		);
+	},
+
+	_mouseDelayMet: function(/* event */) {
+		return this.mouseDelayMet;
+	},
+
+	// These are placeholder methods, to be overriden by extending plugin
+	_mouseStart: function(/* event */) {},
+	_mouseDrag: function(/* event */) {},
+	_mouseStop: function(/* event */) {},
+	_mouseCapture: function(/* event */) { return true; }
+});
+
+})(jQuery);
diff --git a/static/jquery.ui.sortable.js b/static/jquery.ui.sortable.js
new file mode 100644
--- /dev/null
+++ b/static/jquery.ui.sortable.js
@@ -0,0 +1,1250 @@
+/*!
+ * jQuery UI Sortable 1.10.1
+ * http://jqueryui.com
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/sortable/
+ *
+ * Depends:
+ *	jquery.ui.core.js
+ *	jquery.ui.mouse.js
+ *	jquery.ui.widget.js
+ */
+(function( $, undefined ) {
+
+/*jshint loopfunc: true */
+
+function isOverAxis( x, reference, size ) {
+	return ( x > reference ) && ( x < ( reference + size ) );
+}
+
+$.widget("ui.sortable", $.ui.mouse, {
+	version: "1.10.1",
+	widgetEventPrefix: "sort",
+	ready: false,
+	options: {
+		appendTo: "parent",
+		axis: false,
+		connectWith: false,
+		containment: false,
+		cursor: "auto",
+		cursorAt: false,
+		dropOnEmpty: true,
+		forcePlaceholderSize: false,
+		forceHelperSize: false,
+		grid: false,
+		handle: false,
+		helper: "original",
+		items: "> *",
+		opacity: false,
+		placeholder: false,
+		revert: false,
+		scroll: true,
+		scrollSensitivity: 20,
+		scrollSpeed: 20,
+		scope: "default",
+		tolerance: "intersect",
+		zIndex: 1000,
+
+		// callbacks
+		activate: null,
+		beforeStop: null,
+		change: null,
+		deactivate: null,
+		out: null,
+		over: null,
+		receive: null,
+		remove: null,
+		sort: null,
+		start: null,
+		stop: null,
+		update: null
+	},
+	_create: function() {
+
+		var o = this.options;
+		this.containerCache = {};
+		this.element.addClass("ui-sortable");
+
+		//Get the items
+		this.refresh();
+
+		//Let's determine if the items are being displayed horizontally
+		this.floating = this.items.length ? o.axis === "x" || (/left|right/).test(this.items[0].item.css("float")) || (/inline|table-cell/).test(this.items[0].item.css("display")) : false;
+
+		//Let's determine the parent's offset
+		this.offset = this.element.offset();
+
+		//Initialize mouse events for interaction
+		this._mouseInit();
+
+		//We're ready to go
+		this.ready = true;
+
+	},
+
+	_destroy: function() {
+		this.element
+			.removeClass("ui-sortable ui-sortable-disabled");
+		this._mouseDestroy();
+
+		for ( var i = this.items.length - 1; i >= 0; i-- ) {
+			this.items[i].item.removeData(this.widgetName + "-item");
+		}
+
+		return this;
+	},
+
+	_setOption: function(key, value){
+		if ( key === "disabled" ) {
+			this.options[ key ] = value;
+
+			this.widget().toggleClass( "ui-sortable-disabled", !!value );
+		} else {
+			// Don't call widget base _setOption for disable as it adds ui-state-disabled class
+			$.Widget.prototype._setOption.apply(this, arguments);
+		}
+	},
+
+	_mouseCapture: function(event, overrideHandle) {
+		var currentItem = null,
+			validHandle = false,
+			that = this;
+
+		if (this.reverting) {
+			return false;
+		}
+
+		if(this.options.disabled || this.options.type === "static") {
+			return false;
+		}
+
+		//We have to refresh the items data once first
+		this._refreshItems(event);
+
+		//Find out if the clicked node (or one of its parents) is a actual item in this.items
+		$(event.target).parents().each(function() {
+			if($.data(this, that.widgetName + "-item") === that) {
+				currentItem = $(this);
+				return false;
+			}
+		});
+		if($.data(event.target, that.widgetName + "-item") === that) {
+			currentItem = $(event.target);
+		}
+
+		if(!currentItem) {
+			return false;
+		}
+		if(this.options.handle && !overrideHandle) {
+			$(this.options.handle, currentItem).find("*").addBack().each(function() {
+				if(this === event.target) {
+					validHandle = true;
+				}
+			});
+			if(!validHandle) {
+				return false;
+			}
+		}
+
+		this.currentItem = currentItem;
+		this._removeCurrentsFromItems();
+		return true;
+
+	},
+
+	_mouseStart: function(event, overrideHandle, noActivation) {
+
+		var i,
+			o = this.options;
+
+		this.currentContainer = this;
+
+		//We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture
+		this.refreshPositions();
+
+		//Create and append the visible helper
+		this.helper = this._createHelper(event);
+
+		//Cache the helper size
+		this._cacheHelperProportions();
+
+		/*
+		 * - Position generation -
+		 * This block generates everything position related - it's the core of draggables.
+		 */
+
+		//Cache the margins of the original element
+		this._cacheMargins();
+
+		//Get the next scrolling parent
+		this.scrollParent = this.helper.scrollParent();
+
+		//The element's absolute position on the page minus margins
+		this.offset = this.currentItem.offset();
+		this.offset = {
+			top: this.offset.top - this.margins.top,
+			left: this.offset.left - this.margins.left
+		};
+
+		$.extend(this.offset, {
+			click: { //Where the click happened, relative to the element
+				left: event.pageX - this.offset.left,
+				top: event.pageY - this.offset.top
+			},
+			parent: this._getParentOffset(),
+			relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
+		});
+
+		// Only after we got the offset, we can change the helper's position to absolute
+		// TODO: Still need to figure out a way to make relative sorting possible
+		this.helper.css("position", "absolute");
+		this.cssPosition = this.helper.css("position");
+
+		//Generate the original position
+		this.originalPosition = this._generatePosition(event);
+		this.originalPageX = event.pageX;
+		this.originalPageY = event.pageY;
+
+		//Adjust the mouse offset relative to the helper if "cursorAt" is supplied
+		(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
+
+		//Cache the former DOM position
+		this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] };
+
+		//If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way
+		if(this.helper[0] !== this.currentItem[0]) {
+			this.currentItem.hide();
+		}
+
+		//Create the placeholder
+		this._createPlaceholder();
+
+		//Set a containment if given in the options
+		if(o.containment) {
+			this._setContainment();
+		}
+
+		if(o.cursor) { // cursor option
+			if ($("body").css("cursor")) {
+				this._storedCursor = $("body").css("cursor");
+			}
+			$("body").css("cursor", o.cursor);
+		}
+
+		if(o.opacity) { // opacity option
+			if (this.helper.css("opacity")) {
+				this._storedOpacity = this.helper.css("opacity");
+			}
+			this.helper.css("opacity", o.opacity);
+		}
+
+		if(o.zIndex) { // zIndex option
+			if (this.helper.css("zIndex")) {
+				this._storedZIndex = this.helper.css("zIndex");
+			}
+			this.helper.css("zIndex", o.zIndex);
+		}
+
+		//Prepare scrolling
+		if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
+			this.overflowOffset = this.scrollParent.offset();
+		}
+
+		//Call callbacks
+		this._trigger("start", event, this._uiHash());
+
+		//Recache the helper size
+		if(!this._preserveHelperProportions) {
+			this._cacheHelperProportions();
+		}
+
+
+		//Post "activate" events to possible containers
+		if( !noActivation ) {
+			for ( i = this.containers.length - 1; i >= 0; i-- ) {
+				this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) );
+			}
+		}
+
+		//Prepare possible droppables
+		if($.ui.ddmanager) {
+			$.ui.ddmanager.current = this;
+		}
+
+		if ($.ui.ddmanager && !o.dropBehaviour) {
+			$.ui.ddmanager.prepareOffsets(this, event);
+		}
+
+		this.dragging = true;
+
+		this.helper.addClass("ui-sortable-helper");
+		this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position
+		return true;
+
+	},
+
+	_mouseDrag: function(event) {
+		var i, item, itemElement, intersection,
+			o = this.options,
+			scrolled = false;
+
+		//Compute the helpers position
+		this.position = this._generatePosition(event);
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		if (!this.lastPositionAbs) {
+			this.lastPositionAbs = this.positionAbs;
+		}
+
+		//Do scrolling
+		if(this.options.scroll) {
+			if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") {
+
+				if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) {
+					this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed;
+				} else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) {
+					this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed;
+				}
+
+				if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) {
+					this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed;
+				} else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) {
+					this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed;
+				}
+
+			} else {
+
+				if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) {
+					scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
+				} else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) {
+					scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
+				}
+
+				if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) {
+					scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
+				} else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) {
+					scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
+				}
+
+			}
+
+			if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) {
+				$.ui.ddmanager.prepareOffsets(this, event);
+			}
+		}
+
+		//Regenerate the absolute position used for position checks
+		this.positionAbs = this._convertPositionTo("absolute");
+
+		//Set the helper position
+		if(!this.options.axis || this.options.axis !== "y") {
+			this.helper[0].style.left = this.position.left+"px";
+		}
+		if(!this.options.axis || this.options.axis !== "x") {
+			this.helper[0].style.top = this.position.top+"px";
+		}
+
+		//Rearrange
+		for (i = this.items.length - 1; i >= 0; i--) {
+
+			//Cache variables and intersection, continue if no intersection
+			item = this.items[i];
+			itemElement = item.item[0];
+			intersection = this._intersectsWithPointer(item);
+			if (!intersection) {
+				continue;
+			}
+
+			// Only put the placeholder inside the current Container, skip all
+			// items form other containers. This works because when moving
+			// an item from one container to another the
+			// currentContainer is switched before the placeholder is moved.
+			//
+			// Without this moving items in "sub-sortables" can cause the placeholder to jitter
+			// beetween the outer and inner container.
+			if (item.instance !== this.currentContainer) {
+				continue;
+			}
+
+			// cannot intersect with itself
+			// no useless actions that have been done before
+			// no action if the item moved is the parent of the item checked
+			if (itemElement !== this.currentItem[0] &&
+				this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement &&
+				!$.contains(this.placeholder[0], itemElement) &&
+				(this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true)
+			) {
+
+				this.direction = intersection === 1 ? "down" : "up";
+
+				if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) {
+					this._rearrange(event, item);
+				} else {
+					break;
+				}
+
+				this._trigger("change", event, this._uiHash());
+				break;
+			}
+		}
+
+		//Post events to containers
+		this._contactContainers(event);
+
+		//Interconnect with droppables
+		if($.ui.ddmanager) {
+			$.ui.ddmanager.drag(this, event);
+		}
+
+		//Call callbacks
+		this._trigger("sort", event, this._uiHash());
+
+		this.lastPositionAbs = this.positionAbs;
+		return false;
+
+	},
+
+	_mouseStop: function(event, noPropagation) {
+
+		if(!event) {
+			return;
+		}
+
+		//If we are using droppables, inform the manager about the drop
+		if ($.ui.ddmanager && !this.options.dropBehaviour) {
+			$.ui.ddmanager.drop(this, event);
+		}
+
+		if(this.options.revert) {
+			var that = this,
+				cur = this.placeholder.offset();
+
+			this.reverting = true;
+
+			$(this.helper).animate({
+				left: cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft),
+				top: cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop)
+			}, parseInt(this.options.revert, 10) || 500, function() {
+				that._clear(event);
+			});
+		} else {
+			this._clear(event, noPropagation);
+		}
+
+		return false;
+
+	},
+
+	cancel: function() {
+
+		if(this.dragging) {
+
+			this._mouseUp({ target: null });
+
+			if(this.options.helper === "original") {
+				this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+			} else {
+				this.currentItem.show();
+			}
+
+			//Post deactivating events to containers
+			for (var i = this.containers.length - 1; i >= 0; i--){
+				this.containers[i]._trigger("deactivate", null, this._uiHash(this));
+				if(this.containers[i].containerCache.over) {
+					this.containers[i]._trigger("out", null, this._uiHash(this));
+					this.containers[i].containerCache.over = 0;
+				}
+			}
+
+		}
+
+		if (this.placeholder) {
+			//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+			if(this.placeholder[0].parentNode) {
+				this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+			}
+			if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) {
+				this.helper.remove();
+			}
+
+			$.extend(this, {
+				helper: null,
+				dragging: false,
+				reverting: false,
+				_noFinalSort: null
+			});
+
+			if(this.domPosition.prev) {
+				$(this.domPosition.prev).after(this.currentItem);
+			} else {
+				$(this.domPosition.parent).prepend(this.currentItem);
+			}
+		}
+
+		return this;
+
+	},
+
+	serialize: function(o) {
+
+		var items = this._getItemsAsjQuery(o && o.connected),
+			str = [];
+		o = o || {};
+
+		$(items).each(function() {
+			var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/));
+			if (res) {
+				str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2]));
+			}
+		});
+
+		if(!str.length && o.key) {
+			str.push(o.key + "=");
+		}
+
+		return str.join("&");
+
+	},
+
+	toArray: function(o) {
+
+		var items = this._getItemsAsjQuery(o && o.connected),
+			ret = [];
+
+		o = o || {};
+
+		items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); });
+		return ret;
+
+	},
+
+	/* Be careful with the following core functions */
+	_intersectsWith: function(item) {
+
+		var x1 = this.positionAbs.left,
+			x2 = x1 + this.helperProportions.width,
+			y1 = this.positionAbs.top,
+			y2 = y1 + this.helperProportions.height,
+			l = item.left,
+			r = l + item.width,
+			t = item.top,
+			b = t + item.height,
+			dyClick = this.offset.click.top,
+			dxClick = this.offset.click.left,
+			isOverElement = (y1 + dyClick) > t && (y1 + dyClick) < b && (x1 + dxClick) > l && (x1 + dxClick) < r;
+
+		if ( this.options.tolerance === "pointer" ||
+			this.options.forcePointerForContainers ||
+			(this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"])
+		) {
+			return isOverElement;
+		} else {
+
+			return (l < x1 + (this.helperProportions.width / 2) && // Right Half
+				x2 - (this.helperProportions.width / 2) < r && // Left Half
+				t < y1 + (this.helperProportions.height / 2) && // Bottom Half
+				y2 - (this.helperProportions.height / 2) < b ); // Top Half
+
+		}
+	},
+
+	_intersectsWithPointer: function(item) {
+
+		var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height),
+			isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width),
+			isOverElement = isOverElementHeight && isOverElementWidth,
+			verticalDirection = this._getDragVerticalDirection(),
+			horizontalDirection = this._getDragHorizontalDirection();
+
+		if (!isOverElement) {
+			return false;
+		}
+
+		return this.floating ?
+			( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 )
+			: ( verticalDirection && (verticalDirection === "down" ? 2 : 1) );
+
+	},
+
+	_intersectsWithSides: function(item) {
+
+		var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height),
+			isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width),
+			verticalDirection = this._getDragVerticalDirection(),
+			horizontalDirection = this._getDragHorizontalDirection();
+
+		if (this.floating && horizontalDirection) {
+			return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf));
+		} else {
+			return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf));
+		}
+
+	},
+
+	_getDragVerticalDirection: function() {
+		var delta = this.positionAbs.top - this.lastPositionAbs.top;
+		return delta !== 0 && (delta > 0 ? "down" : "up");
+	},
+
+	_getDragHorizontalDirection: function() {
+		var delta = this.positionAbs.left - this.lastPositionAbs.left;
+		return delta !== 0 && (delta > 0 ? "right" : "left");
+	},
+
+	refresh: function(event) {
+		this._refreshItems(event);
+		this.refreshPositions();
+		return this;
+	},
+
+	_connectWith: function() {
+		var options = this.options;
+		return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith;
+	},
+
+	_getItemsAsjQuery: function(connected) {
+
+		var i, j, cur, inst,
+			items = [],
+			queries = [],
+			connectWith = this._connectWith();
+
+		if(connectWith && connected) {
+			for (i = connectWith.length - 1; i >= 0; i--){
+				cur = $(connectWith[i]);
+				for ( j = cur.length - 1; j >= 0; j--){
+					inst = $.data(cur[j], this.widgetFullName);
+					if(inst && inst !== this && !inst.options.disabled) {
+						queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]);
+					}
+				}
+			}
+		}
+
+		queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]);
+
+		for (i = queries.length - 1; i >= 0; i--){
+			queries[i][0].each(function() {
+				items.push(this);
+			});
+		}
+
+		return $(items);
+
+	},
+
+	_removeCurrentsFromItems: function() {
+
+		var list = this.currentItem.find(":data(" + this.widgetName + "-item)");
+
+		this.items = $.grep(this.items, function (item) {
+			for (var j=0; j < list.length; j++) {
+				if(list[j] === item.item[0]) {
+					return false;
+				}
+			}
+			return true;
+		});
+
+	},
+
+	_refreshItems: function(event) {
+
+		this.items = [];
+		this.containers = [this];
+
+		var i, j, cur, inst, targetData, _queries, item, queriesLength,
+			items = this.items,
+			queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]],
+			connectWith = this._connectWith();
+
+		if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down
+			for (i = connectWith.length - 1; i >= 0; i--){
+				cur = $(connectWith[i]);
+				for (j = cur.length - 1; j >= 0; j--){
+					inst = $.data(cur[j], this.widgetFullName);
+					if(inst && inst !== this && !inst.options.disabled) {
+						queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]);
+						this.containers.push(inst);
+					}
+				}
+			}
+		}
+
+		for (i = queries.length - 1; i >= 0; i--) {
+			targetData = queries[i][1];
+			_queries = queries[i][0];
+
+			for (j=0, queriesLength = _queries.length; j < queriesLength; j++) {
+				item = $(_queries[j]);
+
+				item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager)
+
+				items.push({
+					item: item,
+					instance: targetData,
+					width: 0, height: 0,
+					left: 0, top: 0
+				});
+			}
+		}
+
+	},
+
+	refreshPositions: function(fast) {
+
+		//This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change
+		if(this.offsetParent && this.helper) {
+			this.offset.parent = this._getParentOffset();
+		}
+
+		var i, item, t, p;
+
+		for (i = this.items.length - 1; i >= 0; i--){
+			item = this.items[i];
+
+			//We ignore calculating positions of all connected containers when we're not over them
+			if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) {
+				continue;
+			}
+
+			t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item;
+
+			if (!fast) {
+				item.width = t.outerWidth();
+				item.height = t.outerHeight();
+			}
+
+			p = t.offset();
+			item.left = p.left;
+			item.top = p.top;
+		}
+
+		if(this.options.custom && this.options.custom.refreshContainers) {
+			this.options.custom.refreshContainers.call(this);
+		} else {
+			for (i = this.containers.length - 1; i >= 0; i--){
+				p = this.containers[i].element.offset();
+				this.containers[i].containerCache.left = p.left;
+				this.containers[i].containerCache.top = p.top;
+				this.containers[i].containerCache.width	= this.containers[i].element.outerWidth();
+				this.containers[i].containerCache.height = this.containers[i].element.outerHeight();
+			}
+		}
+
+		return this;
+	},
+
+	_createPlaceholder: function(that) {
+		that = that || this;
+		var className,
+			o = that.options;
+
+		if(!o.placeholder || o.placeholder.constructor === String) {
+			className = o.placeholder;
+			o.placeholder = {
+				element: function() {
+
+					var el = $(document.createElement(that.currentItem[0].nodeName))
+						.addClass(className || that.currentItem[0].className+" ui-sortable-placeholder")
+						.removeClass("ui-sortable-helper")[0];
+
+					if(!className) {
+						el.style.visibility = "hidden";
+					}
+
+					return el;
+				},
+				update: function(container, p) {
+
+					// 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that
+					// 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified
+					if(className && !o.forcePlaceholderSize) {
+						return;
+					}
+
+					//If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item
+					if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); }
+					if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); }
+				}
+			};
+		}
+
+		//Create the placeholder
+		that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem));
+
+		//Append it after the actual current item
+		that.currentItem.after(that.placeholder);
+
+		//Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
+		o.placeholder.update(that, that.placeholder);
+
+	},
+
+	_contactContainers: function(event) {
+		var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom,
+			innermostContainer = null,
+			innermostIndex = null;
+
+		// get innermost container that intersects with item
+		for (i = this.containers.length - 1; i >= 0; i--) {
+
+			// never consider a container that's located within the item itself
+			if($.contains(this.currentItem[0], this.containers[i].element[0])) {
+				continue;
+			}
+
+			if(this._intersectsWith(this.containers[i].containerCache)) {
+
+				// if we've already found a container and it's more "inner" than this, then continue
+				if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) {
+					continue;
+				}
+
+				innermostContainer = this.containers[i];
+				innermostIndex = i;
+
+			} else {
+				// container doesn't intersect. trigger "out" event if necessary
+				if(this.containers[i].containerCache.over) {
+					this.containers[i]._trigger("out", event, this._uiHash(this));
+					this.containers[i].containerCache.over = 0;
+				}
+			}
+
+		}
+
+		// if no intersecting containers found, return
+		if(!innermostContainer) {
+			return;
+		}
+
+		// move the item into the container if it's not there already
+		if(this.containers.length === 1) {
+			this.containers[innermostIndex]._trigger("over", event, this._uiHash(this));
+			this.containers[innermostIndex].containerCache.over = 1;
+		} else {
+
+			//When entering a new container, we will find the item with the least distance and append our item near it
+			dist = 10000;
+			itemWithLeastDistance = null;
+			posProperty = this.containers[innermostIndex].floating ? "left" : "top";
+			sizeProperty = this.containers[innermostIndex].floating ? "width" : "height";
+			base = this.positionAbs[posProperty] + this.offset.click[posProperty];
+			for (j = this.items.length - 1; j >= 0; j--) {
+				if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) {
+					continue;
+				}
+				if(this.items[j].item[0] === this.currentItem[0]) {
+					continue;
+				}
+				cur = this.items[j].item.offset()[posProperty];
+				nearBottom = false;
+				if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){
+					nearBottom = true;
+					cur += this.items[j][sizeProperty];
+				}
+
+				if(Math.abs(cur - base) < dist) {
+					dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j];
+					this.direction = nearBottom ? "up": "down";
+				}
+			}
+
+			//Check if dropOnEmpty is enabled
+			if(!itemWithLeastDistance && !this.options.dropOnEmpty) {
+				return;
+			}
+
+			this.currentContainer = this.containers[innermostIndex];
+			itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true);
+			this._trigger("change", event, this._uiHash());
+			this.containers[innermostIndex]._trigger("change", event, this._uiHash(this));
+
+			//Update the placeholder
+			this.options.placeholder.update(this.currentContainer, this.placeholder);
+
+			this.containers[innermostIndex]._trigger("over", event, this._uiHash(this));
+			this.containers[innermostIndex].containerCache.over = 1;
+		}
+
+
+	},
+
+	_createHelper: function(event) {
+
+		var o = this.options,
+			helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem);
+
+		//Add the helper to the DOM if that didn't happen already
+		if(!helper.parents("body").length) {
+			$(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]);
+		}
+
+		if(helper[0] === this.currentItem[0]) {
+			this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") };
+		}
+
+		if(!helper[0].style.width || o.forceHelperSize) {
+			helper.width(this.currentItem.width());
+		}
+		if(!helper[0].style.height || o.forceHelperSize) {
+			helper.height(this.currentItem.height());
+		}
+
+		return helper;
+
+	},
+
+	_adjustOffsetFromHelper: function(obj) {
+		if (typeof obj === "string") {
+			obj = obj.split(" ");
+		}
+		if ($.isArray(obj)) {
+			obj = {left: +obj[0], top: +obj[1] || 0};
+		}
+		if ("left" in obj) {
+			this.offset.click.left = obj.left + this.margins.left;
+		}
+		if ("right" in obj) {
+			this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
+		}
+		if ("top" in obj) {
+			this.offset.click.top = obj.top + this.margins.top;
+		}
+		if ("bottom" in obj) {
+			this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
+		}
+	},
+
+	_getParentOffset: function() {
+
+
+		//Get the offsetParent and cache its position
+		this.offsetParent = this.helper.offsetParent();
+		var po = this.offsetParent.offset();
+
+		// This is a special case where we need to modify a offset calculated on start, since the following happened:
+		// 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent
+		// 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that
+		//    the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag
+		if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) {
+			po.left += this.scrollParent.scrollLeft();
+			po.top += this.scrollParent.scrollTop();
+		}
+
+		// This needs to be actually done for all browsers, since pageX/pageY includes this information
+		// with an ugly IE fix
+		if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) {
+			po = { top: 0, left: 0 };
+		}
+
+		return {
+			top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0),
+			left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0)
+		};
+
+	},
+
+	_getRelativeOffset: function() {
+
+		if(this.cssPosition === "relative") {
+			var p = this.currentItem.position();
+			return {
+				top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(),
+				left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft()
+			};
+		} else {
+			return { top: 0, left: 0 };
+		}
+
+	},
+
+	_cacheMargins: function() {
+		this.margins = {
+			left: (parseInt(this.currentItem.css("marginLeft"),10) || 0),
+			top: (parseInt(this.currentItem.css("marginTop"),10) || 0)
+		};
+	},
+
+	_cacheHelperProportions: function() {
+		this.helperProportions = {
+			width: this.helper.outerWidth(),
+			height: this.helper.outerHeight()
+		};
+	},
+
+	_setContainment: function() {
+
+		var ce, co, over,
+			o = this.options;
+		if(o.containment === "parent") {
+			o.containment = this.helper[0].parentNode;
+		}
+		if(o.containment === "document" || o.containment === "window") {
+			this.containment = [
+				0 - this.offset.relative.left - this.offset.parent.left,
+				0 - this.offset.relative.top - this.offset.parent.top,
+				$(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left,
+				($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top
+			];
+		}
+
+		if(!(/^(document|window|parent)$/).test(o.containment)) {
+			ce = $(o.containment)[0];
+			co = $(o.containment).offset();
+			over = ($(ce).css("overflow") !== "hidden");
+
+			this.containment = [
+				co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left,
+				co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top,
+				co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left,
+				co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top
+			];
+		}
+
+	},
+
+	_convertPositionTo: function(d, pos) {
+
+		if(!pos) {
+			pos = this.position;
+		}
+		var mod = d === "absolute" ? 1 : -1,
+			scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent,
+			scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+		return {
+			top: (
+				pos.top	+																// The absolute mouse position
+				this.offset.relative.top * mod +										// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.top * mod -											// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod)
+			),
+			left: (
+				pos.left +																// The absolute mouse position
+				this.offset.relative.left * mod +										// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.left * mod	-										// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod)
+			)
+		};
+
+	},
+
+	_generatePosition: function(event) {
+
+		var top, left,
+			o = this.options,
+			pageX = event.pageX,
+			pageY = event.pageY,
+			scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);
+
+		// This is another very weird special case that only happens for relative elements:
+		// 1. If the css position is relative
+		// 2. and the scroll parent is the document or similar to the offset parent
+		// we have to refresh the relative offset during the scroll so there are no jumps
+		if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) {
+			this.offset.relative = this._getRelativeOffset();
+		}
+
+		/*
+		 * - Position constraining -
+		 * Constrain the position to a mix of grid, containment.
+		 */
+
+		if(this.originalPosition) { //If we are not dragging yet, we won't check for options
+
+			if(this.containment) {
+				if(event.pageX - this.offset.click.left < this.containment[0]) {
+					pageX = this.containment[0] + this.offset.click.left;
+				}
+				if(event.pageY - this.offset.click.top < this.containment[1]) {
+					pageY = this.containment[1] + this.offset.click.top;
+				}
+				if(event.pageX - this.offset.click.left > this.containment[2]) {
+					pageX = this.containment[2] + this.offset.click.left;
+				}
+				if(event.pageY - this.offset.click.top > this.containment[3]) {
+					pageY = this.containment[3] + this.offset.click.top;
+				}
+			}
+
+			if(o.grid) {
+				top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1];
+				pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;
+
+				left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0];
+				pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;
+			}
+
+		}
+
+		return {
+			top: (
+				pageY -																// The absolute mouse position
+				this.offset.click.top -													// Click offset (relative to the element)
+				this.offset.relative.top	-											// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.top +												// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))
+			),
+			left: (
+				pageX -																// The absolute mouse position
+				this.offset.click.left -												// Click offset (relative to the element)
+				this.offset.relative.left	-											// Only for relative positioned nodes: Relative offset from element to offset parent
+				this.offset.parent.left +												// The offsetParent's offset without borders (offset + border)
+				( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))
+			)
+		};
+
+	},
+
+	_rearrange: function(event, i, a, hardRefresh) {
+
+		a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling));
+
+		//Various things done here to improve the performance:
+		// 1. we create a setTimeout, that calls refreshPositions
+		// 2. on the instance, we have a counter variable, that get's higher after every append
+		// 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same
+		// 4. this lets only the last addition to the timeout stack through
+		this.counter = this.counter ? ++this.counter : 1;
+		var counter = this.counter;
+
+		this._delay(function() {
+			if(counter === this.counter) {
+				this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove
+			}
+		});
+
+	},
+
+	_clear: function(event, noPropagation) {
+
+		this.reverting = false;
+		// We delay all events that have to be triggered to after the point where the placeholder has been removed and
+		// everything else normalized again
+		var i,
+			delayedTriggers = [];
+
+		// We first have to update the dom position of the actual currentItem
+		// Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088)
+		if(!this._noFinalSort && this.currentItem.parent().length) {
+			this.placeholder.before(this.currentItem);
+		}
+		this._noFinalSort = null;
+
+		if(this.helper[0] === this.currentItem[0]) {
+			for(i in this._storedCSS) {
+				if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") {
+					this._storedCSS[i] = "";
+				}
+			}
+			this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper");
+		} else {
+			this.currentItem.show();
+		}
+
+		if(this.fromOutside && !noPropagation) {
+			delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); });
+		}
+		if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) {
+			delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed
+		}
+
+		// Check if the items Container has Changed and trigger appropriate
+		// events.
+		if (this !== this.currentContainer) {
+			if(!noPropagation) {
+				delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); });
+				delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); };  }).call(this, this.currentContainer));
+				delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this));  }; }).call(this, this.currentContainer));
+			}
+		}
+
+
+		//Post events to containers
+		for (i = this.containers.length - 1; i >= 0; i--){
+			if(!noPropagation) {
+				delayedTriggers.push((function(c) { return function(event) { c._trigger("deactivate", event, this._uiHash(this)); };  }).call(this, this.containers[i]));
+			}
+			if(this.containers[i].containerCache.over) {
+				delayedTriggers.push((function(c) { return function(event) { c._trigger("out", event, this._uiHash(this)); };  }).call(this, this.containers[i]));
+				this.containers[i].containerCache.over = 0;
+			}
+		}
+
+		//Do what was originally in plugins
+		if(this._storedCursor) {
+			$("body").css("cursor", this._storedCursor);
+		}
+		if(this._storedOpacity) {
+			this.helper.css("opacity", this._storedOpacity);
+		}
+		if(this._storedZIndex) {
+			this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex);
+		}
+
+		this.dragging = false;
+		if(this.cancelHelperRemoval) {
+			if(!noPropagation) {
+				this._trigger("beforeStop", event, this._uiHash());
+				for (i=0; i < delayedTriggers.length; i++) {
+					delayedTriggers[i].call(this, event);
+				} //Trigger all delayed events
+				this._trigger("stop", event, this._uiHash());
+			}
+
+			this.fromOutside = false;
+			return false;
+		}
+
+		if(!noPropagation) {
+			this._trigger("beforeStop", event, this._uiHash());
+		}
+
+		//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node!
+		this.placeholder[0].parentNode.removeChild(this.placeholder[0]);
+
+		if(this.helper[0] !== this.currentItem[0]) {
+			this.helper.remove();
+		}
+		this.helper = null;
+
+		if(!noPropagation) {
+			for (i=0; i < delayedTriggers.length; i++) {
+				delayedTriggers[i].call(this, event);
+			} //Trigger all delayed events
+			this._trigger("stop", event, this._uiHash());
+		}
+
+		this.fromOutside = false;
+		return true;
+
+	},
+
+	_trigger: function() {
+		if ($.Widget.prototype._trigger.apply(this, arguments) === false) {
+			this.cancel();
+		}
+	},
+
+	_uiHash: function(_inst) {
+		var inst = _inst || this;
+		return {
+			helper: inst.helper,
+			placeholder: inst.placeholder || $([]),
+			position: inst.position,
+			originalPosition: inst.originalPosition,
+			offset: inst.positionAbs,
+			item: inst.currentItem,
+			sender: _inst ? _inst.element : null
+		};
+	}
+
+});
+
+})(jQuery);
diff --git a/static/jquery.ui.widget.js b/static/jquery.ui.widget.js
new file mode 100644
--- /dev/null
+++ b/static/jquery.ui.widget.js
@@ -0,0 +1,521 @@
+/*!
+ * jQuery UI Widget 1.10.1
+ * http://jqueryui.com
+ *
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license.
+ * http://jquery.org/license
+ *
+ * http://api.jqueryui.com/jQuery.widget/
+ */
+(function( $, undefined ) {
+
+var uuid = 0,
+	slice = Array.prototype.slice,
+	_cleanData = $.cleanData;
+$.cleanData = function( elems ) {
+	for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+		try {
+			$( elem ).triggerHandler( "remove" );
+		// http://bugs.jquery.com/ticket/8235
+		} catch( e ) {}
+	}
+	_cleanData( elems );
+};
+
+$.widget = function( name, base, prototype ) {
+	var fullName, existingConstructor, constructor, basePrototype,
+		// proxiedPrototype allows the provided prototype to remain unmodified
+		// so that it can be used as a mixin for multiple widgets (#8876)
+		proxiedPrototype = {},
+		namespace = name.split( "." )[ 0 ];
+
+	name = name.split( "." )[ 1 ];
+	fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	// create selector for plugin
+	$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
+		return !!$.data( elem, fullName );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	existingConstructor = $[ namespace ][ name ];
+	constructor = $[ namespace ][ name ] = function( options, element ) {
+		// allow instantiation without "new" keyword
+		if ( !this._createWidget ) {
+			return new constructor( options, element );
+		}
+
+		// allow instantiation without initializing for simple inheritance
+		// must use "new" keyword (the code above always passes args)
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+	// extend with the existing constructor to carry over any static properties
+	$.extend( constructor, existingConstructor, {
+		version: prototype.version,
+		// copy the object used to create the prototype in case we need to
+		// redefine the widget later
+		_proto: $.extend( {}, prototype ),
+		// track widgets that inherit from this widget in case this widget is
+		// redefined after a widget inherits from it
+		_childConstructors: []
+	});
+
+	basePrototype = new base();
+	// we need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+	basePrototype.options = $.widget.extend( {}, basePrototype.options );
+	$.each( prototype, function( prop, value ) {
+		if ( !$.isFunction( value ) ) {
+			proxiedPrototype[ prop ] = value;
+			return;
+		}
+		proxiedPrototype[ prop ] = (function() {
+			var _super = function() {
+					return base.prototype[ prop ].apply( this, arguments );
+				},
+				_superApply = function( args ) {
+					return base.prototype[ prop ].apply( this, args );
+				};
+			return function() {
+				var __super = this._super,
+					__superApply = this._superApply,
+					returnValue;
+
+				this._super = _super;
+				this._superApply = _superApply;
+
+				returnValue = value.apply( this, arguments );
+
+				this._super = __super;
+				this._superApply = __superApply;
+
+				return returnValue;
+			};
+		})();
+	});
+	constructor.prototype = $.widget.extend( basePrototype, {
+		// TODO: remove support for widgetEventPrefix
+		// always use the name + a colon as the prefix, e.g., draggable:start
+		// don't prefix for widgets that aren't DOM-based
+		widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name
+	}, proxiedPrototype, {
+		constructor: constructor,
+		namespace: namespace,
+		widgetName: name,
+		widgetFullName: fullName
+	});
+
+	// If this widget is being redefined then we need to find all widgets that
+	// are inheriting from it and redefine all of them so that they inherit from
+	// the new version of this widget. We're essentially trying to replace one
+	// level in the prototype chain.
+	if ( existingConstructor ) {
+		$.each( existingConstructor._childConstructors, function( i, child ) {
+			var childPrototype = child.prototype;
+
+			// redefine the child widget using the same prototype that was
+			// originally used, but inherit from the new version of the base
+			$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
+		});
+		// remove the list of existing child constructors from the old constructor
+		// so the old child constructors can be garbage collected
+		delete existingConstructor._childConstructors;
+	} else {
+		base._childConstructors.push( constructor );
+	}
+
+	$.widget.bridge( name, constructor );
+};
+
+$.widget.extend = function( target ) {
+	var input = slice.call( arguments, 1 ),
+		inputIndex = 0,
+		inputLength = input.length,
+		key,
+		value;
+	for ( ; inputIndex < inputLength; inputIndex++ ) {
+		for ( key in input[ inputIndex ] ) {
+			value = input[ inputIndex ][ key ];
+			if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
+				// Clone objects
+				if ( $.isPlainObject( value ) ) {
+					target[ key ] = $.isPlainObject( target[ key ] ) ?
+						$.widget.extend( {}, target[ key ], value ) :
+						// Don't extend strings, arrays, etc. with objects
+						$.widget.extend( {}, value );
+				// Copy everything else by reference
+				} else {
+					target[ key ] = value;
+				}
+			}
+		}
+	}
+	return target;
+};
+
+$.widget.bridge = function( name, object ) {
+	var fullName = object.prototype.widgetFullName || name;
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string",
+			args = slice.call( arguments, 1 ),
+			returnValue = this;
+
+		// allow multiple hashes to be passed on init
+		options = !isMethodCall && args.length ?
+			$.widget.extend.apply( null, [ options ].concat(args) ) :
+			options;
+
+		if ( isMethodCall ) {
+			this.each(function() {
+				var methodValue,
+					instance = $.data( this, fullName );
+				if ( !instance ) {
+					return $.error( "cannot call methods on " + name + " prior to initialization; " +
+						"attempted to call method '" + options + "'" );
+				}
+				if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
+					return $.error( "no such method '" + options + "' for " + name + " widget instance" );
+				}
+				methodValue = instance[ options ].apply( instance, args );
+				if ( methodValue !== instance && methodValue !== undefined ) {
+					returnValue = methodValue && methodValue.jquery ?
+						returnValue.pushStack( methodValue.get() ) :
+						methodValue;
+					return false;
+				}
+			});
+		} else {
+			this.each(function() {
+				var instance = $.data( this, fullName );
+				if ( instance ) {
+					instance.option( options || {} )._init();
+				} else {
+					$.data( this, fullName, new object( options, this ) );
+				}
+			});
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( /* options, element */ ) {};
+$.Widget._childConstructors = [];
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	defaultElement: "<div>",
+	options: {
+		disabled: false,
+
+		// callbacks
+		create: null
+	},
+	_createWidget: function( options, element ) {
+		element = $( element || this.defaultElement || this )[ 0 ];
+		this.element = $( element );
+		this.uuid = uuid++;
+		this.eventNamespace = "." + this.widgetName + this.uuid;
+		this.options = $.widget.extend( {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		this.bindings = $();
+		this.hoverable = $();
+		this.focusable = $();
+
+		if ( element !== this ) {
+			$.data( element, this.widgetFullName, this );
+			this._on( true, this.element, {
+				remove: function( event ) {
+					if ( event.target === element ) {
+						this.destroy();
+					}
+				}
+			});
+			this.document = $( element.style ?
+				// element within the document
+				element.ownerDocument :
+				// element is window or document
+				element.document || element );
+			this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
+		}
+
+		this._create();
+		this._trigger( "create", null, this._getCreateEventData() );
+		this._init();
+	},
+	_getCreateOptions: $.noop,
+	_getCreateEventData: $.noop,
+	_create: $.noop,
+	_init: $.noop,
+
+	destroy: function() {
+		this._destroy();
+		// we can probably remove the unbind calls in 2.0
+		// all event bindings should go through this._on()
+		this.element
+			.unbind( this.eventNamespace )
+			// 1.9 BC for #7810
+			// TODO remove dual storage
+			.removeData( this.widgetName )
+			.removeData( this.widgetFullName )
+			// support: jquery <1.6.3
+			// http://bugs.jquery.com/ticket/9413
+			.removeData( $.camelCase( this.widgetFullName ) );
+		this.widget()
+			.unbind( this.eventNamespace )
+			.removeAttr( "aria-disabled" )
+			.removeClass(
+				this.widgetFullName + "-disabled " +
+				"ui-state-disabled" );
+
+		// clean up events and states
+		this.bindings.unbind( this.eventNamespace );
+		this.hoverable.removeClass( "ui-state-hover" );
+		this.focusable.removeClass( "ui-state-focus" );
+	},
+	_destroy: $.noop,
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key,
+			parts,
+			curOption,
+			i;
+
+		if ( arguments.length === 0 ) {
+			// don't return a reference to the internal hash
+			return $.widget.extend( {}, this.options );
+		}
+
+		if ( typeof key === "string" ) {
+			// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
+			options = {};
+			parts = key.split( "." );
+			key = parts.shift();
+			if ( parts.length ) {
+				curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
+				for ( i = 0; i < parts.length - 1; i++ ) {
+					curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
+					curOption = curOption[ parts[ i ] ];
+				}
+				key = parts.pop();
+				if ( value === undefined ) {
+					return curOption[ key ] === undefined ? null : curOption[ key ];
+				}
+				curOption[ key ] = value;
+			} else {
+				if ( value === undefined ) {
+					return this.options[ key ] === undefined ? null : this.options[ key ];
+				}
+				options[ key ] = value;
+			}
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+	_setOptions: function( options ) {
+		var key;
+
+		for ( key in options ) {
+			this._setOption( key, options[ key ] );
+		}
+
+		return this;
+	},
+	_setOption: function( key, value ) {
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this.widget()
+				.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
+				.attr( "aria-disabled", value );
+			this.hoverable.removeClass( "ui-state-hover" );
+			this.focusable.removeClass( "ui-state-focus" );
+		}
+
+		return this;
+	},
+
+	enable: function() {
+		return this._setOption( "disabled", false );
+	},
+	disable: function() {
+		return this._setOption( "disabled", true );
+	},
+
+	_on: function( suppressDisabledCheck, element, handlers ) {
+		var delegateElement,
+			instance = this;
+
+		// no suppressDisabledCheck flag, shuffle arguments
+		if ( typeof suppressDisabledCheck !== "boolean" ) {
+			handlers = element;
+			element = suppressDisabledCheck;
+			suppressDisabledCheck = false;
+		}
+
+		// no element argument, shuffle and use this.element
+		if ( !handlers ) {
+			handlers = element;
+			element = this.element;
+			delegateElement = this.widget();
+		} else {
+			// accept selectors, DOM elements
+			element = delegateElement = $( element );
+			this.bindings = this.bindings.add( element );
+		}
+
+		$.each( handlers, function( event, handler ) {
+			function handlerProxy() {
+				// allow widgets to customize the disabled handling
+				// - disabled as an array instead of boolean
+				// - disabled class as method for disabling individual parts
+				if ( !suppressDisabledCheck &&
+						( instance.options.disabled === true ||
+							$( this ).hasClass( "ui-state-disabled" ) ) ) {
+					return;
+				}
+				return ( typeof handler === "string" ? instance[ handler ] : handler )
+					.apply( instance, arguments );
+			}
+
+			// copy the guid so direct unbinding works
+			if ( typeof handler !== "string" ) {
+				handlerProxy.guid = handler.guid =
+					handler.guid || handlerProxy.guid || $.guid++;
+			}
+
+			var match = event.match( /^(\w+)\s*(.*)$/ ),
+				eventName = match[1] + instance.eventNamespace,
+				selector = match[2];
+			if ( selector ) {
+				delegateElement.delegate( selector, eventName, handlerProxy );
+			} else {
+				element.bind( eventName, handlerProxy );
+			}
+		});
+	},
+
+	_off: function( element, eventName ) {
+		eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace;
+		element.unbind( eventName ).undelegate( eventName );
+	},
+
+	_delay: function( handler, delay ) {
+		function handlerProxy() {
+			return ( typeof handler === "string" ? instance[ handler ] : handler )
+				.apply( instance, arguments );
+		}
+		var instance = this;
+		return setTimeout( handlerProxy, delay || 0 );
+	},
+
+	_hoverable: function( element ) {
+		this.hoverable = this.hoverable.add( element );
+		this._on( element, {
+			mouseenter: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-hover" );
+			},
+			mouseleave: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-hover" );
+			}
+		});
+	},
+
+	_focusable: function( element ) {
+		this.focusable = this.focusable.add( element );
+		this._on( element, {
+			focusin: function( event ) {
+				$( event.currentTarget ).addClass( "ui-state-focus" );
+			},
+			focusout: function( event ) {
+				$( event.currentTarget ).removeClass( "ui-state-focus" );
+			}
+		});
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig,
+			callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+		// the original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+		return !( $.isFunction( callback ) &&
+			callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
+	$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
+		if ( typeof options === "string" ) {
+			options = { effect: options };
+		}
+		var hasOptions,
+			effectName = !options ?
+				method :
+				options === true || typeof options === "number" ?
+					defaultEffect :
+					options.effect || defaultEffect;
+		options = options || {};
+		if ( typeof options === "number" ) {
+			options = { duration: options };
+		}
+		hasOptions = !$.isEmptyObject( options );
+		options.complete = callback;
+		if ( options.delay ) {
+			element.delay( options.delay );
+		}
+		if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
+			element[ method ]( options );
+		} else if ( effectName !== method && element[ effectName ] ) {
+			element[ effectName ]( options.duration, options.easing, callback );
+		} else {
+			element.queue(function( next ) {
+				$( this )[ method ]();
+				if ( callback ) {
+					callback.call( element[ 0 ] );
+				}
+				next();
+			});
+		}
+	};
+});
+
+})( jQuery );
diff --git a/static/longpolling.js b/static/longpolling.js
--- a/static/longpolling.js
+++ b/static/longpolling.js
@@ -3,12 +3,15 @@
 
 connfails=0;
 
+longpollcallbacks = $.Callbacks();
+
 function longpoll(url, divid, cont, fail) {
 	$.ajax({
 		'url': url,
 		'dataType': 'html',
 		'success': function(data, status, jqxhr) {
 			$('#' + divid).replaceWith(data);
+			longpollcallbacks.fire();
 			connfails=0;
 			cont();
 		},
diff --git a/templates/configurators/intro.hamlet b/templates/configurators/intro.hamlet
--- a/templates/configurators/intro.hamlet
+++ b/templates/configurators/intro.hamlet
@@ -13,7 +13,7 @@
         \ with these repositories:
         <table .table .table-striped .table-condensed>
           <tbody>
-            $forall (num, name, _) <- repolist
+            $forall (num, name, _, _) <- repolist
               <tr>
                 <td>
                   #{num}
diff --git a/templates/configurators/main.hamlet b/templates/configurators/main.hamlet
--- a/templates/configurators/main.hamlet
+++ b/templates/configurators/main.hamlet
@@ -2,11 +2,11 @@
   <div .row-fluid>
     <div .span4>
       <h3>
-        <a href="@{RepositoriesR}">
-          Manage repositories
-     <p>
-       Distribute the files in this repository to other devices, #
-       make backups, and more, by adding repositories.
+        <a href="@{PreferencesR}">
+          Preferences
+      <p>
+        Tune the behavior of git-annex, including how many copies #
+        to retain of each file, and how much disk space it can use.
     <div .span4>
       $if xmppconfigured
         <h3>
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
@@ -6,7 +6,7 @@
     <br>
     Do you want to combine it with your existing repository at #{mainrepo}?
   <p>
-    <a .btn href="@{CombineRepositoryR newrepopath newrepouuid}">
+    <a .btn href="@{CombineRepositoryR $ FilePathAndUUID newrepopath newrepouuid}">
       <i .icon-resize-small></i> Combine the repositories #
     The combined repositories will sync and share their files.
   <p>
diff --git a/templates/configurators/pairing/xmpp/confirm.hamlet b/templates/configurators/pairing/xmpp/confirm.hamlet
--- a/templates/configurators/pairing/xmpp/confirm.hamlet
+++ b/templates/configurators/pairing/xmpp/confirm.hamlet
@@ -7,5 +7,5 @@
   <p>
     <a .btn .btn-primary .btn-large href="@{FinishXMPPPairR pairkey}">
       Accept pair request
-    <a .btn .btn-large href="@{HomeR}">
+    <a .btn .btn-large href="@{DashboardR}">
       No thanks
diff --git a/templates/configurators/pairing/xmpp/end.hamlet b/templates/configurators/pairing/xmpp/end.hamlet
--- a/templates/configurators/pairing/xmpp/end.hamlet
+++ b/templates/configurators/pairing/xmpp/end.hamlet
@@ -1,14 +1,16 @@
 <div .span9 .hero-unit>
   $if inprogress
     <h2>
-      Pairing in progress ...
+      Pair request sent ...
     <p>
       $maybe name <- friend
-        A pair request has been sent to #{name}. It's up to them #
+        A pair request has been sent to #{name}. Now it's up to them #
         to accept it and finish pairing.
       $nothing
-        A pair request has been sent to all other devices using your jabber #
-        account.
+        A pair request has been sent to all other devices that #
+        have been configured to use your jabber account. #
+        It will be answered automatically by any that see it; #
+        no action is required on your part.
   $else
     Pair request accepted.
   <h2>
@@ -28,4 +30,4 @@
   ^{cloudrepolist}
   <h2>
     Add a cloud repository
-  ^{makeCloudRepositories}
+  ^{makeCloudRepositories True}
diff --git a/templates/configurators/pairing/xmpp/prompt.hamlet b/templates/configurators/pairing/xmpp/prompt.hamlet
--- a/templates/configurators/pairing/xmpp/prompt.hamlet
+++ b/templates/configurators/pairing/xmpp/prompt.hamlet
@@ -6,6 +6,6 @@
     into a single shared repository, with changes kept in sync.
   <p>
     You can pair with any of your friends using jabber, or with another #
-    computer that shares your own jabber account.
+    device that shares your own jabber account.
     <p>
       ^{buddyListDisplay}
diff --git a/templates/configurators/preferences.hamlet b/templates/configurators/preferences.hamlet
new file mode 100644
--- /dev/null
+++ b/templates/configurators/preferences.hamlet
@@ -0,0 +1,13 @@
+<div .span9 .hero-unit>
+  <h2>
+    Preferences
+  <p>
+    <form .form-horizontal enctype=#{enctype}>
+      <fieldset>
+        ^{form}
+        ^{webAppFormAuthToken}
+        <div .form-actions>
+          <button .btn .btn-primary type=submit>
+            Save Preferences
+          <a .btn href="@{ConfigurationR}">
+            Cancel
diff --git a/templates/configurators/repositories.hamlet b/templates/configurators/repositories.hamlet
--- a/templates/configurators/repositories.hamlet
+++ b/templates/configurators/repositories.hamlet
@@ -11,4 +11,4 @@
       <h2>
         Store your data in the cloud
 
-      ^{makeCloudRepositories}
+      ^{makeCloudRepositories False}
diff --git a/templates/configurators/repositories/cloud.hamlet b/templates/configurators/repositories/cloud.hamlet
--- a/templates/configurators/repositories/cloud.hamlet
+++ b/templates/configurators/repositories/cloud.hamlet
@@ -16,11 +16,12 @@
 <p>
   Good choice for professional quality storage.
 
-<h3>
-  <a href="@{AddGlacierR}">
-    <i .icon-plus-sign></i> Amazon Glacier
-<p>
-  Low cost offline data archival.
+$if not onlyTransfer
+  <h3>
+    <a href="@{AddGlacierR}">
+      <i .icon-plus-sign></i> Amazon Glacier
+  <p>
+    Low cost offline data archival.
 
 <h3>
   <a href="@{AddSshR}">
diff --git a/templates/configurators/repositories/list.hamlet b/templates/configurators/repositories/list.hamlet
--- a/templates/configurators/repositories/list.hamlet
+++ b/templates/configurators/repositories/list.hamlet
@@ -10,14 +10,13 @@
       <h2>
         Repositories
   <table .table .table-condensed>
-    <tbody>
-      $forall (num, name, actions) <- repolist
-        <tr>
-          <td>
-            #{num}
-          <td>
+    <tbody #costsortable>
+      $forall (_num, name, uuid, actions) <- repolist
+        <tr .repoline ##{fromUUID uuid}>
+          <td .handle>
+            <i .icon-resize-vertical></i>
             #{name}
-          <td>
+          <td .draghide>
             $if needsEnabled actions
               <a href="@{setupRepoLink actions}">
                 <i .icon-warning-sign></i> not enabled
@@ -27,7 +26,7 @@
                   <i .icon-pause></i> syncing paused
                 $else
                   <i .icon-refresh></i> syncing enabled
-          <td>
+          <td .draghide>
             $if needsEnabled actions
               <a href="@{setupRepoLink actions}">
                 enable
diff --git a/templates/configurators/repositories/list.julius b/templates/configurators/repositories/list.julius
new file mode 100644
--- /dev/null
+++ b/templates/configurators/repositories/list.julius
@@ -0,0 +1,27 @@
+$(function() {
+	var setup = function() {
+		$("#costsortable").sortable({
+			handle: ".handle",
+			cursor: "move",
+			forceHelperSize: true,
+			start: function(event, ui) {
+				ui.item.children(".draghide").hide();
+			},
+			stop: function(event, ui) {
+				ui.item.children(".draghide").show();
+				var list = $("#costsortable").sortable("toArray");
+				var moved = ui.item.attr("id");
+				$.ajax({
+					'url': "@{RepositoriesReorderR}",
+					'data': {
+						'moved': moved,
+						'list': list
+					}
+				});
+			},
+		});
+	};
+	longpollcallbacks.add(setup);
+	setup();
+
+});
diff --git a/templates/configurators/xmpp/buddylist.hamlet b/templates/configurators/xmpp/buddylist.hamlet
--- a/templates/configurators/xmpp/buddylist.hamlet
+++ b/templates/configurators/xmpp/buddylist.hamlet
@@ -3,8 +3,11 @@
     <tbody>
     $if null buddies
       <tr>
-        <td> 
-          Nobody is currently available.
+        <td>
+          $if isNothing myjid
+            Not connected to the jabber server. Check your network connection ...
+          $else
+            Nobody is currently available.
     $else
       $forall (name, away, canpair, paired, buddyid) <- buddies
         <tr>
diff --git a/templates/control/shutdown.hamlet b/templates/control/shutdown.hamlet
--- a/templates/control/shutdown.hamlet
+++ b/templates/control/shutdown.hamlet
@@ -4,5 +4,5 @@
   <p>
     <a .btn .btn-danger href="@{ShutdownConfirmedR}">
       <i .icon-off></i> Confirm shutdown
-    <a .btn href="@{HomeR}">
+    <a .btn href="@{DashboardR}">
       Keep running
diff --git a/templates/documentation/about.hamlet b/templates/documentation/about.hamlet
--- a/templates/documentation/about.hamlet
+++ b/templates/documentation/about.hamlet
@@ -26,3 +26,5 @@
     . &hearts;
     <p>
     Version: #{packageversion}
+    <br>
+    Build flags: #{unwords buildFlags}
